Repository: googleapis/java-storage
Pull request #2445 (2024-03-12 23:09:27)

feat: add hierarchical namespace and folders features
Adds support for hierarchical namespace and folders. Copied over from an internal workspace


Patch:
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index ba323387..635e882a 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -15,6 +15,12 @@
     <method>* writeAndClose(*)</method>
   </difference>
 
+  <difference>
+    <differenceType>7013</differenceType>
+    <className>com/google/cloud/storage/BucketInfo$Builder</className>
+    <method>com.google.cloud.storage.BucketInfo$Builder setHierarchicalNamespace(com.google.cloud.storage.BucketInfo$HierarchicalNamespace)</method>
+  </difference>
+
   <difference>
     <differenceType>7013</differenceType>
     <className>com/google/cloud/storage/BlobInfo$Builder</className>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index a7138c4b..604b126f 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
-    <pubsub-proto.version>1.109.1</pubsub-proto.version>
+    <pubsub-proto.version>1.109.2</pubsub-proto.version>
     <junit-platform.version>5.10.2</junit-platform.version>
   </properties>
   <dependencies>
@@ -173,13 +173,13 @@
     <dependency>
       <groupId>com.google.api.grpc</groupId>
       <artifactId>proto-google-cloud-kms-v1</artifactId>
-      <version>0.130.0</version>
+      <version>0.131.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-kms</artifactId>
-      <version>2.39.0</version>
+      <version>2.40.0</version>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
index 3c1652bc..6459e0a0 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Bucket.java
@@ -748,6 +748,12 @@ public class Bucket extends BucketInfo {
       return this;
     }
 
+    @Override
+    public Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace) {
+      infoBuilder.setHierarchicalNamespace(hierarchicalNamespace);
+      return this;
+    }
+
     @Override
     public Bucket build() {
       return new Bucket(storage, infoBuilder);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
index 9fb46b5b..af3e4436 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BucketInfo.java
@@ -118,6 +118,7 @@ public class BucketInfo implements Serializable {
   private final Logging logging;
   private final CustomPlacementConfig customPlacementConfig;
   private final ObjectRetention objectRetention;
+  private final HierarchicalNamespace hierarchicalNamespace;
 
   private final transient ImmutableSet<NamedField> modifiedFields;
 
@@ -713,6 +714,71 @@ public class BucketInfo implements Serializable {
     }
   }
 
+  /** The bucket's hierarchical namespace (Folders) configuration. Enable this to use HNS. */
+  public static final class HierarchicalNamespace implements Serializable {
+
+    private static final long serialVersionUID = 5932926691444613101L;
+    private Boolean enabled;
+
+    public Boolean getEnabled() {
+      return enabled;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof HierarchicalNamespace)) {
+        return false;
+      }
+      HierarchicalNamespace that = (HierarchicalNamespace) o;
+      return Objects.equals(enabled, that.enabled);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(enabled);
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("enabled", enabled).toString();
+    }
+
+    private HierarchicalNamespace() {}
+
+    private HierarchicalNamespace(Builder builder) {
+      this.enabled = builder.enabled;
+    }
+
+    public static Builder newBuilder() {
+      return new Builder();
+    }
+
+    public Builder toBuilder() {
+      return newBuilder().setEnabled(enabled);
+    }
+
+    public static final class Builder {
+      private Boolean enabled;
+
+      /**
+       * Sets whether Hierarchical Namespace (Folders) is enabled for this bucket. This can only be
+       * enabled at bucket create time. If this is enabled, Uniform Bucket-Level Access must also be
+       * enabled.
+       */
+      public Builder setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+        return this;
+      }
+
+      public HierarchicalNamespace build() {
+        return new HierarchicalNamespace(this);
+      }
+    }
+  }
+
   /**
    * Lifecycle rule for a bucket. Allows supported Actions, such as deleting and changing storage
    * class, to be executed when certain Conditions are met.
@@ -1683,6 +1749,8 @@ public class BucketInfo implements Serializable {
 
     public abstract Builder setCustomPlacementConfig(CustomPlacementConfig customPlacementConfig);
 
+    public abstract Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace);
+
     abstract Builder setObjectRetention(ObjectRetention objectRetention);
 
     /** Creates a {@code BucketInfo} object. */
@@ -1783,6 +1851,7 @@ public class BucketInfo implements Serializable {
     private Logging logging;
     private CustomPlacementConfig customPlacementConfig;
     private ObjectRetention objectRetention;
+    private HierarchicalNamespace hierarchicalNamespace;
     private final ImmutableSet.Builder<NamedField> modifiedFields = ImmutableSet.builder();
 
     BuilderImpl(String name) {
@@ -1822,6 +1891,7 @@ public class BucketInfo implements Serializable {
       logging = bucketInfo.logging;
       customPlacementConfig = bucketInfo.customPlacementConfig;
       objectRetention = bucketInfo.objectRetention;
+      hierarchicalNamespace = bucketInfo.hierarchicalNamespace;
     }
 
     @Override
@@ -2187,6 +2257,15 @@ public class BucketInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setHierarchicalNamespace(HierarchicalNamespace hierarchicalNamespace) {
+      if (!Objects.equals(this.hierarchicalNamespace, hierarchicalNamespace)) {
+        modifiedFields.add(BucketField.HIERARCHICAL_NAMESPACE);
+      }
+      this.hierarchicalNamespace = hierarchicalNamespace;
+      return this;
+    }
+
     @Override
     Builder setLocationType(String locationType) {
       if (!Objects.equals(this.locationType, locationType)) {
@@ -2428,6 +2507,7 @@ public class BucketInfo implements Serializable {
     logging = builder.logging;
     customPlacementConfig = builder.customPlacementConfig;
     objectRetention = builder.objectRetention;
+    hierarchicalNamespace = builder.hierarchicalNamespace;
     modifiedFields = builder.modifiedFields.build();
   }
 
@@ -2768,6 +2848,11 @@ public class BucketInfo implements Serializable {
     return objectRetention;
   }
 
+  /** Returns the Hierarchical Namespace (Folders) Configuration */
+  public HierarchicalNamespace getHierarchicalNamespace() {
+    return hierarchicalNamespace;
+  }
+
   /** Returns a builder for the current bucket. */
   public Builder toBuilder() {
     return new BuilderImpl(this);
@@ -2805,6 +2890,7 @@ public class BucketInfo implements Serializable {
         autoclass,
         locationType,
         objectRetention,
+        hierarchicalNamespace,
         logging);
   }
 
@@ -2846,6 +2932,7 @@ public class BucketInfo implements Serializable {
         && Objects.equals(autoclass, that.autoclass)
         && Objects.equals(locationType, that.locationType)
         && Objects.equals(objectRetention, that.objectRetention)
+        && Objects.equals(hierarchicalNamespace, that.hierarchicalNamespace)
         && Objects.equals(logging, that.logging);
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
index 9ddf229c..d1084bc4 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
@@ -110,6 +110,10 @@ final class GrpcConversions {
   private final Codec<Condition, Expr> iamConditionCodec =
       Codec.of(this::conditionEncode, this::conditionDecode);
 
+  private final Codec<BucketInfo.HierarchicalNamespace, Bucket.HierarchicalNamespace>
+      hierarchicalNamespaceCodec =
+          Codec.of(this::hierarchicalNamespaceEncode, this::hierarchicalNamespaceDecode);
+
   @VisibleForTesting
   final Codec<OffsetDateTime, Timestamp> timestampCodec =
       Codec.of(
@@ -297,6 +301,10 @@ final class GrpcConversions {
               .setDataLocations(customPlacementConfig.getDataLocationsList())
               .build());
     }
+    if (from.hasHierarchicalNamespace()) {
+      to.setHierarchicalNamespace(
+          hierarchicalNamespaceCodec.decode(from.getHierarchicalNamespace()));
+    }
     // TODO(frankyn): Add SelfLink when the field is available
     if (!from.getEtag().isEmpty()) {
       to.setEtag(from.getEtag());
@@ -382,6 +390,10 @@ final class GrpcConversions {
               .addAllDataLocations(customPlacementConfig.getDataLocations())
               .build());
     }
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        hierarchicalNamespaceCodec::encode,
+        to::setHierarchicalNamespace);
     // TODO(frankyn): Add SelfLink when the field is available
     ifNonNull(from.getEtag(), to::setEtag);
     return to.build();
@@ -589,6 +601,20 @@ final class GrpcConversions {
     return to.build();
   }
 
+  private Bucket.HierarchicalNamespace hierarchicalNamespaceEncode(
+      BucketInfo.HierarchicalNamespace from) {
+    Bucket.HierarchicalNamespace.Builder to = Bucket.HierarchicalNamespace.newBuilder();
+    ifNonNull(from.getEnabled(), to::setEnabled);
+    return to.build();
+  }
+
+  private BucketInfo.HierarchicalNamespace hierarchicalNamespaceDecode(
+      Bucket.HierarchicalNamespace from) {
+    BucketInfo.HierarchicalNamespace.Builder to = BucketInfo.HierarchicalNamespace.newBuilder();
+    to.setEnabled(from.getEnabled());
+    return to.build();
+  }
+
   private Bucket.IamConfig iamConfigEncode(BucketInfo.IamConfiguration from) {
     Bucket.IamConfig.Builder to = Bucket.IamConfig.newBuilder();
     to.setUniformBucketLevelAccess(ublaEncode(from));
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
index dcadfb40..841343d3 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
@@ -767,6 +767,7 @@ public final class GrpcStorageOptions extends StorageOptions
 
   private static final class NoopGrpcInterceptorProvider
       implements GrpcInterceptorProvider, Serializable {
+    private static long serialVersionUID = -8523033236999805349L;
     private static final NoopGrpcInterceptorProvider INSTANCE = new NoopGrpcInterceptorProvider();
 
     @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
index 9d4873e2..fd2a5731 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
@@ -36,6 +36,8 @@ import com.google.cloud.storage.spi.v1.HttpStorageRpc;
 import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.io.Serializable;
 import java.util.Set;
 
@@ -52,7 +54,7 @@ public class HttpStorageOptions extends StorageOptions {
   private static final String DEFAULT_HOST = "https://storage.googleapis.com";
 
   private final HttpRetryAlgorithmManager retryAlgorithmManager;
-  private final RetryDependenciesAdapter retryDepsAdapter;
+  private transient RetryDependenciesAdapter retryDepsAdapter;
 
   private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) {
     super(builder, serviceDefaults);
@@ -93,6 +95,11 @@ public class HttpStorageOptions extends StorageOptions {
     return obj instanceof HttpStorageOptions && baseEquals((HttpStorageOptions) obj);
   }
 
+  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+    in.defaultReadObject();
+    this.retryDepsAdapter = new RetryDependenciesAdapter();
+  }
+
   public static HttpStorageOptions.Builder newBuilder() {
     return new HttpStorageOptions.Builder().setHost(DEFAULT_HOST);
   }
@@ -345,8 +352,7 @@ public class HttpStorageOptions extends StorageOptions {
    * We don't yet want to make HttpStorageOptions itself implement {@link RetryingDependencies} but
    * we do need use it in a couple places, for those we create this adapter.
    */
-  private final class RetryDependenciesAdapter implements RetryingDependencies, Serializable {
-
+  private final class RetryDependenciesAdapter implements RetryingDependencies {
     private RetryDependenciesAdapter() {}
 
     @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
index 4bbf08d4..c5add241 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
@@ -137,6 +137,10 @@ final class JsonConversions {
   private final Codec<BlobInfo, StorageObject> blobInfoCodec =
       Codec.of(this::blobInfoEncode, this::blobInfoDecode);
 
+  private final Codec<BucketInfo.HierarchicalNamespace, Bucket.HierarchicalNamespace>
+      hierarchicalNamespaceCodec =
+          Codec.of(this::hierarchicalNamespaceEncode, this::hierarchicalNamespaceDecode);
+
   private final Codec<NotificationInfo, com.google.api.services.storage.model.Notification>
       notificationInfoCodec = Codec.of(this::notificationEncode, this::notificationDecode);
   private final Codec<CustomPlacementConfig, Bucket.CustomPlacementConfig>
@@ -437,6 +441,10 @@ final class JsonConversions {
         this::customPlacementConfigEncode,
         to::setCustomPlacementConfig);
     ifNonNull(from.getObjectRetention(), this::objectRetentionEncode, to::setObjectRetention);
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        this::hierarchicalNamespaceEncode,
+        to::setHierarchicalNamespace);
     return to;
   }
 
@@ -487,6 +495,10 @@ final class JsonConversions {
         from.getCustomPlacementConfig(),
         this::customPlacementConfigDecode,
         to::setCustomPlacementConfig);
+    ifNonNull(
+        from.getHierarchicalNamespace(),
+        this::hierarchicalNamespaceDecode,
+        to::setHierarchicalNamespace);
     ifNonNull(from.getObjectRetention(), this::objectRetentionDecode, to::setObjectRetention);
     return to.build();
   }
@@ -861,6 +873,20 @@ final class JsonConversions {
     return to;
   }
 
+  private Bucket.HierarchicalNamespace hierarchicalNamespaceEncode(
+      BucketInfo.HierarchicalNamespace from) {
+    Bucket.HierarchicalNamespace to = new Bucket.HierarchicalNamespace();
+    ifNonNull(from.getEnabled(), to::setEnabled);
+    return to;
+  }
+
+  private BucketInfo.HierarchicalNamespace hierarchicalNamespaceDecode(
+      Bucket.HierarchicalNamespace from) {
+    BucketInfo.HierarchicalNamespace.Builder to = BucketInfo.HierarchicalNamespace.newBuilder();
+    to.setEnabled(from.getEnabled());
+    return to.build();
+  }
+
   private NotificationInfo notificationDecode(
       com.google.api.services.storage.model.Notification from) {
     NotificationInfo.Builder builder = new NotificationInfo.BuilderImpl(from.getTopic());
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
index f3e2bb81..f3e0738d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
@@ -26,6 +26,7 @@ import java.util.Objects;
  * details.
  */
 public class Notification extends NotificationInfo {
+  private static final long serialVersionUID = 3150928330690874200L;
 
   private final StorageOptions options;
   private transient Storage storage;
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 5ca31c04..1d0ae834 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
@@ -159,6 +159,9 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
     CUSTOM_PLACEMENT_CONFIG("customPlacementConfig", "custom_placement_config"),
     @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     AUTOCLASS("autoclass"),
+
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    HIERARCHICAL_NAMESPACE("hierarchicalNamespace", "hierarchical_namespace"),
     @TransportCompatibility({Transport.HTTP})
     OBJECT_RETENTION("objectRetention");
 
@@ -1788,6 +1791,14 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobListOption(UnifiedOpts.matchGlob(glob));
     }
 
+    /**
+     * Returns an option for whether to include all Folders (including empty Folders) in response.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobListOption includeFolders(boolean includeFolders) {
+      return new BlobListOption(UnifiedOpts.includeFoldersAsPrefixes(includeFolders));
+    }
+
     /**
      * Returns an option to define the billing user project. This option is required by buckets with
      * `requester_pays` flag enabled to assign operation costs.
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
index 74a6ffb3..53ad6142 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
@@ -63,6 +63,8 @@ public abstract class StorageOptions extends ServiceOptions<Storage, StorageOpti
   /** @deprecated Use {@link HttpStorageFactory} */
   @Deprecated
   public static class DefaultStorageFactory extends HttpStorageFactory {
+    private static final long serialVersionUID = -7856840922014956661L;
+
     /** @deprecated Use {@link HttpStorageDefaults#getDefaultServiceFactory()} */
     @Deprecated
     public DefaultStorageFactory() {
@@ -73,6 +75,7 @@ public abstract class StorageOptions extends ServiceOptions<Storage, StorageOpti
   /** @deprecated Use {@link HttpStorageRpcFactory} */
   @Deprecated
   public static class DefaultStorageRpcFactory extends HttpStorageRpcFactory {
+    private static final long serialVersionUID = -7856840922014956661L;
 
     /** @deprecated Use {@link HttpStorageDefaults#getDefaultRpcFactory()} */
     @Deprecated
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index dbf7bb64..3159cbeb 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -370,6 +370,10 @@ final class UnifiedOpts {
     return new Delimiter(delimiter);
   }
 
+  static IncludeFoldersAsPrefixes includeFoldersAsPrefixes(boolean includeFoldersAsPrefixes) {
+    return new IncludeFoldersAsPrefixes(includeFoldersAsPrefixes);
+  }
+
   @Deprecated
   static DetectContentType detectContentType() {
     return DetectContentType.INSTANCE;
@@ -636,6 +640,20 @@ final class UnifiedOpts {
     }
   }
 
+  static final class IncludeFoldersAsPrefixes extends RpcOptVal<Boolean> implements ObjectListOpt {
+
+    private static final long serialVersionUID = 321916692864878282L;
+
+    private IncludeFoldersAsPrefixes(boolean val) {
+      super(StorageRpc.Option.INCLUDE_FOLDERS_AS_PREFIXES, val);
+    }
+
+    @Override
+    public Mapper<ListObjectsRequest.Builder> listObjects() {
+      return b -> b.setIncludeFoldersAsPrefixes(val);
+    }
+  }
+
   static final class Delimiter extends RpcOptVal<String> implements ObjectListOpt {
     private static final long serialVersionUID = -3789556789947615714L;
 
@@ -2540,6 +2558,7 @@ final class UnifiedOpts {
   }
 
   private static final class PrefixedNamedField implements NamedField {
+    private static long serialVersionUID = -4899304145424680141L;
 
     private final String prefix;
     private final NamedField delegate;
@@ -2586,6 +2605,7 @@ final class UnifiedOpts {
   }
 
   private static final class LiteralNamedField implements NamedField {
+    private static long serialVersionUID = 1422947423774466409L;
 
     private final String name;
 
@@ -2627,6 +2647,7 @@ final class UnifiedOpts {
   }
 
   private static final class NestedNamedField implements NamedField {
+    private static long serialVersionUID = -7623005572810688221L;
     private final NamedField parent;
     private final NamedField child;
 
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 2f1c3e21..3ca2eabe 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
@@ -459,6 +459,7 @@ public class HttpStorageRpc implements StorageRpc {
               .setPageToken(Option.PAGE_TOKEN.getString(options))
               .setFields(Option.FIELDS.getString(options))
               .setUserProject(Option.USER_PROJECT.getString(options))
+              .setIncludeFoldersAsPrefixes(Option.INCLUDE_FOLDERS_AS_PREFIXES.getBoolean(options))
               .execute();
       Iterable<StorageObject> storageObjects =
           Iterables.concat(
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 7d671dee..3b40f6a2 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
@@ -73,7 +73,9 @@ public interface StorageRpc extends ServiceRpc {
     DETECT_CONTENT_TYPE("detectContentType"),
     ENABLE_OBJECT_RETENTION("enableObjectRetention"),
     RETURN_RAW_INPUT_STREAM("returnRawInputStream"),
-    OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention");
+    OVERRIDE_UNLOCKED_RETENTION("overrideUnlockedRetention"),
+    INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes");
+    ;
 
     private final String value;
 
diff --git a/pom.xml b/pom.xml
index 6a1297ca..98845612 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,7 +86,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.127.1</version>
+        <version>1.127.2</version>
         <scope>test</scope>
       </dependency>
       <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 4603242f..2f8aca59 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.127.1</version>
+      <version>1.127.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index db9dad2f..2f4f14a4 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.127.1</version>
+      <version>1.127.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index d875f8e6..6211bd42 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -52,7 +52,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.127.1</version>
+      <version>1.127.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index da562bc0..b9c0b93c 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -72,7 +72,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.127.1</version>
+      <version>1.127.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
index 93099d41..9c977603 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketReadMaskTest.java
@@ -133,6 +133,7 @@ public final class ITBucketReadMaskTest {
               new Args<>(BucketField.TIME_CREATED, LazyAssertion.equal()),
               new Args<>(BucketField.UPDATED, LazyAssertion.equal()),
               new Args<>(BucketField.VERSIONING, LazyAssertion.equal()),
+              new Args<>(BucketField.HIERARCHICAL_NAMESPACE, LazyAssertion.equal()),
               new Args<>(BucketField.WEBSITE, LazyAssertion.equal()));
 
       List<String> argsDefined =
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
index e32be5c3..5b759be6 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBucketTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.api.gax.paging.Page;
+import com.google.api.services.storage.model.Folder;
 import com.google.cloud.Policy;
 import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.BlobInfo;
@@ -44,6 +45,7 @@ import com.google.cloud.storage.Storage.BucketGetOption;
 import com.google.cloud.storage.Storage.BucketListOption;
 import com.google.cloud.storage.Storage.BucketTargetOption;
 import com.google.cloud.storage.StorageClass;
+import com.google.cloud.storage.StorageOptions;
 import com.google.cloud.storage.TestUtils;
 import com.google.cloud.storage.TransportCompatibility.Transport;
 import com.google.cloud.storage.it.runner.StorageITRunner;
@@ -53,6 +55,7 @@ import com.google.cloud.storage.it.runner.annotations.BucketType;
 import com.google.cloud.storage.it.runner.annotations.CrossRun;
 import com.google.cloud.storage.it.runner.annotations.Inject;
 import com.google.cloud.storage.it.runner.registry.Generator;
+import com.google.cloud.storage.spi.v1.HttpStorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.time.Duration;
@@ -551,6 +554,79 @@ public class ITBucketTest {
     }
   }
 
+  @Test
+  public void createBucketWithHierarchicalNamespace() {
+    String bucketName = generator.randomBucketName();
+    storage.create(
+        BucketInfo.newBuilder(bucketName)
+            .setHierarchicalNamespace(
+                BucketInfo.HierarchicalNamespace.newBuilder().setEnabled(true).build())
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(true)
+                    .build())
+            .build());
+    try {
+      Bucket remoteBucket = storage.get(bucketName);
+      assertNotNull(remoteBucket.getHierarchicalNamespace());
+      assertTrue(remoteBucket.getHierarchicalNamespace().getEnabled());
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
+  @Test
+  public void testListObjectsWithFolders() throws Exception {
+    String bucketName = generator.randomBucketName();
+    storage.create(
+        BucketInfo.newBuilder(bucketName)
+            .setHierarchicalNamespace(
+                BucketInfo.HierarchicalNamespace.newBuilder().setEnabled(true).build())
+            .setIamConfiguration(
+                BucketInfo.IamConfiguration.newBuilder()
+                    .setIsUniformBucketLevelAccessEnabled(true)
+                    .build())
+            .build());
+    try {
+      com.google.api.services.storage.Storage apiaryStorage =
+          new HttpStorageRpc(StorageOptions.getDefaultInstance()).getStorage();
+      apiaryStorage
+          .folders()
+          .insert(bucketName, new Folder().setName("F").setBucket(bucketName))
+          .execute();
+
+      Page<Blob> blobs =
+          storage.list(
+              bucketName,
+              Storage.BlobListOption.delimiter("/"),
+              Storage.BlobListOption.includeFolders(false));
+
+      boolean found = false;
+      for (Blob blob : blobs.iterateAll()) {
+        if (blob.getName().equals("F/")) {
+          found = true;
+        }
+      }
+      assert (!found);
+
+      blobs =
+          storage.list(
+              bucketName,
+              Storage.BlobListOption.delimiter("/"),
+              Storage.BlobListOption.includeFolders(true));
+
+      for (Blob blob : blobs.iterateAll()) {
+        if (blob.getName().equals("F/")) {
+          found = true;
+        }
+      }
+      assert (found);
+
+    } finally {
+      BucketCleaner.doCleanup(bucketName, storage);
+    }
+  }
+
   private void unsetRequesterPays() {
     Bucket remoteBucket =
         storage.get(
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
index 4dbe55ad..7627bf3a 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITOptionRegressionTest.java
@@ -326,6 +326,7 @@ public final class ITOptionRegressionTest {
             "timeCreated",
             "updated",
             "versioning",
+            "hierarchicalNamespace",
             "website");
     s.get(
         b.getName(),
@@ -816,6 +817,7 @@ public final class ITOptionRegressionTest {
             "items/timeCreated",
             "items/updated",
             "items/versioning",
+            "items/hierarchicalNamespace",
             "items/website");
     s.list(BucketListOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values())));
     requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet());