Repository: googleapis/java-storage
Pull request #1105 (2021-10-13 20:57:54)

feat: Add RPO metadata settings
Adds RPO metadata settings for buckets.  

Holding off on adding documentation until this general implementation is approved 

See https://github.com/googleapis/google-cloud-java/pull/7700


Patch:
diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml
index 731a0eb4..dddb13b8 100644
--- a/.github/.OwlBot.lock.yaml
+++ b/.github/.OwlBot.lock.yaml
@@ -1,3 +1,3 @@
 docker:
   image: gcr.io/cloud-devrel-public-resources/owlbot-java:latest
-  digest: sha256:491a007c6bd6e77f9e9b1bebcd6cdf08a4a4ef2c228c123d9696845204cb685d
+  digest: sha256:7f737996c88586c499f7b0c25294c760f9f5eec092289a0e08d2d3f42a069ebb
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index dcbafdd9..cef26db6 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -21,7 +21,7 @@ If you are still having issues, please include as much information as possible:
    General, Core, and Other are also allowed as types
 2. OS type and version:
 3. Java version:
-4. storage version(s):
+4.  version(s):
 
 #### Steps to reproduce
 
diff --git a/.repo-metadata.json b/.repo-metadata.json
index b258be10..19bef9ed 100644
--- a/.repo-metadata.json
+++ b/.repo-metadata.json
@@ -1,5 +1,5 @@
 {
-  "name": "storage",
+  "api_shortname": "storage",
   "name_pretty": "Cloud Storage",
   "product_documentation": "https://cloud.google.com/storage",
   "client_documentation": "https://cloud.google.com/java/docs/reference/google-cloud-storage/latest/history",
diff --git a/README.md b/README.md
index 42a0678c..74b8cdce 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>24.1.2</version>
+      <version>24.2.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -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.2.2</version>
+  <version>2.2.3</version>
 </dependency>
 
 ```
@@ -49,7 +49,7 @@ If you are using Maven without BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:24.1.2')
+implementation platform('com.google.cloud:libraries-bom:24.2.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index 18931676..c45b439c 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -1,6 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- see https://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
 <differences>
+    <difference>
+        <className>com/google/cloud/storage/BucketInfo$Builder</className>
+        <method>com.google.cloud.storage.BucketInfo$Builder setRpo(com.google.cloud.storage.Rpo)</method>
+        <differenceType>7013</differenceType>
+    </difference>
     <difference>
         <className>com/google/cloud/storage/BucketInfo$LifecycleRule$LifecycleAction</className>
         <method>BucketInfo$LifecycleRule$LifecycleAction()</method>
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 3714e6e9..95acd19d 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
@@ -577,6 +577,12 @@ public class Bucket extends BucketInfo {
       return this;
     }
 
+    @Override
+    public Builder setRpo(Rpo rpo) {
+      infoBuilder.setRpo(rpo);
+      return this;
+    }
+
     @Override
     public Builder setStorageClass(StorageClass storageClass) {
       infoBuilder.setStorageClass(storageClass);
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 2404a7c1..5e06b6ce 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
@@ -92,6 +92,7 @@ public class BucketInfo implements Serializable {
   private final List<Acl> acl;
   private final List<Acl> defaultAcl;
   private final String location;
+  private final Rpo rpo;
   private final StorageClass storageClass;
   private final Map<String, String> labels;
   private final String defaultKmsKeyName;
@@ -1205,6 +1206,15 @@ public class BucketInfo implements Serializable {
     /** Deletes the lifecycle rules of this bucket. */
     public abstract Builder deleteLifecycleRules();
 
+    /**
+     * Sets the bucket's Recovery Point Objective (RPO). This can only be set for a dual-region
+     * bucket, and determines the speed at which data will be replicated between regions. See the
+     * {@code Rpo} class for supported values, and <a
+     * href="https://cloud.google.com/storage/docs/turbo-replication">here</a> for additional
+     * details.
+     */
+    public abstract Builder setRpo(Rpo rpo);
+
     /**
      * Sets the bucket's storage class. This defines how blobs in the bucket are stored and
      * determines the SLA and the cost of storage. A list of supported values is available <a
@@ -1306,6 +1316,7 @@ public class BucketInfo implements Serializable {
     private String notFoundPage;
     private List<DeleteRule> deleteRules;
     private List<LifecycleRule> lifecycleRules;
+    private Rpo rpo;
     private StorageClass storageClass;
     private String location;
     private String etag;
@@ -1337,6 +1348,7 @@ public class BucketInfo implements Serializable {
       updateTime = bucketInfo.updateTime;
       metageneration = bucketInfo.metageneration;
       location = bucketInfo.location;
+      rpo = bucketInfo.rpo;
       storageClass = bucketInfo.storageClass;
       cors = bucketInfo.cors;
       acl = bucketInfo.acl;
@@ -1430,6 +1442,12 @@ public class BucketInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setRpo(Rpo rpo) {
+      this.rpo = rpo;
+      return this;
+    }
+
     @Override
     public Builder setStorageClass(StorageClass storageClass) {
       this.storageClass = storageClass;
@@ -1568,6 +1586,7 @@ public class BucketInfo implements Serializable {
     updateTime = builder.updateTime;
     metageneration = builder.metageneration;
     location = builder.location;
+    rpo = builder.rpo;
     storageClass = builder.storageClass;
     cors = builder.cors;
     acl = builder.acl;
@@ -1729,6 +1748,16 @@ public class BucketInfo implements Serializable {
     return locationType;
   }
 
+  /**
+   * Returns the bucket's recovery point objective (RPO). This defines how quickly data is
+   * replicated between regions in a dual-region bucket. Not defined for single-region buckets.
+   *
+   * @see <a href="https://cloud.google.com/storage/docs/turbo-replication"Turbo Replication"</a>
+   */
+  public Rpo getRpo() {
+    return rpo;
+  }
+
   /**
    * Returns the bucket's storage class. This defines how blobs in the bucket are stored and
    * determines the SLA and the cost of storage.
@@ -1899,6 +1928,9 @@ public class BucketInfo implements Serializable {
     if (locationType != null) {
       bucketPb.setLocationType(locationType);
     }
+    if (rpo != null) {
+      bucketPb.setRpo(rpo.toString());
+    }
     if (storageClass != null) {
       bucketPb.setStorageClass(storageClass.toString());
     }
@@ -2062,6 +2094,9 @@ public class BucketInfo implements Serializable {
     if (bucketPb.getLocation() != null) {
       builder.setLocation(bucketPb.getLocation());
     }
+    if (bucketPb.getRpo() != null) {
+      builder.setRpo(Rpo.valueOf(bucketPb.getRpo()));
+    }
     if (bucketPb.getStorageClass() != null) {
       builder.setStorageClass(StorageClass.valueOf(bucketPb.getStorageClass()));
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Rpo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Rpo.java
new file mode 100644
index 00000000..ca6a4b0a
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Rpo.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.storage;
+
+import com.google.api.core.ApiFunction;
+import com.google.cloud.StringEnumType;
+import com.google.cloud.StringEnumValue;
+
+/**
+ * Enums for the Recovery Point Objective (RPO) of dual-region buckets, which determines how fast
+ * data is replicated between regions.
+ *
+ * @see <a
+ *     href="https://cloud.google.com/storage/docs/turbo-replication">https://cloud.google.com/storage/docs/turbo-replication</a>
+ */
+public final class Rpo extends StringEnumValue {
+
+  private static final long serialVersionUID = -3954216195295821508L;
+
+  private Rpo(String constant) {
+    super(constant);
+  }
+
+  private static final ApiFunction<String, Rpo> CONSTRUCTOR = Rpo::new;
+
+  private static final StringEnumType<Rpo> type = new StringEnumType<>(Rpo.class, CONSTRUCTOR);
+
+  /**
+   * Default recovery point objective. With this setting, there is no guarantee on the amount of
+   * time it takes for data to replicate between regions.
+   */
+  public static final Rpo DEFAULT = type.createAndRegister("DEFAULT");
+
+  /**
+   * Turbo recovery point objective. With this setting, data in a dual-region bucket will replicate
+   * between regions within 15 minutes.
+   */
+  public static final Rpo ASYNC_TURBO = type.createAndRegister("ASYNC_TURBO");
+
+  /**
+   * Get the Rpo for the given String constant, and throw an exception if the constant is not
+   * recognized.
+   */
+  public static Rpo valueOfStrict(String constant) {
+    return type.valueOfStrict(constant);
+  }
+
+  /** Get the Rpo for the given String constant, and allow unrecognized values. */
+  public static Rpo valueOf(String constant) {
+    return type.valueOf(constant);
+  }
+
+  /** Return the known values for Rpo. */
+  public static Rpo[] values() {
+    return type.values();
+  }
+}
diff --git a/pom.xml b/pom.xml
index ddd607d4..aecfdac1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.2.4</version>
+    <version>1.2.5</version>
   </parent>
 
   <developers>
@@ -80,7 +80,7 @@
       <dependency>
         <groupId>org.easymock</groupId>
         <artifactId>easymock</artifactId>
-        <version>3.6</version>
+        <version>4.3</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -99,7 +99,7 @@
       <dependency>
         <groupId>org.objenesis</groupId>
         <artifactId>objenesis</artifactId>
-        <version>2.6</version>
+        <version>3.2</version>
         <scope>test</scope>
       </dependency>
       <dependency>
@@ -120,7 +120,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-conformance-tests</artifactId>
-        <version>0.2.5</version>
+        <version>0.2.6</version>
         <scope>test</scope>
         <exclusions>
           <exclusion>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 8bed3824..7acbcf5f 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.2.2</version>
+      <version>2.2.3</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index d107c56c..b512cf00 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.2.2</version>
+      <version>2.2.3</version>
     </dependency>
 
     <dependency>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 16b471a3..eab24165 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -30,7 +30,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>24.1.2</version>
+        <version>24.2.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>


Test patch:
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 2307f037..ad8dc6c0 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
@@ -69,9 +69,11 @@ import com.google.cloud.storage.CopyWriter;
 import com.google.cloud.storage.Cors;
 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.PostPolicyV4;
 import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
+import com.google.cloud.storage.Rpo;
 import com.google.cloud.storage.ServiceAccount;
 import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobField;
@@ -2426,8 +2428,11 @@ public class ITStorageTest {
 
   @Test
   public void testHmacKey() {
-    ServiceAccount serviceAccount = ServiceAccount.of(System.getenv("IT_SERVICE_ACCOUNT_EMAIL"));
+    String serviceAccountEmail = System.getenv("IT_SERVICE_ACCOUNT_EMAIL");
+    assertNotNull("Unable to determine service account email", serviceAccountEmail);
+    ServiceAccount serviceAccount = ServiceAccount.of(serviceAccountEmail);
     try {
+      cleanUpHmacKeys(serviceAccount);
 
       HmacKey hmacKey = storage.createHmacKey(serviceAccount);
       String secretKey = hmacKey.getSecretKey();
@@ -2500,10 +2505,18 @@ public class ITStorageTest {
 
       assertEquals(2, Iterators.size(metadatas.getValues().iterator()));
     } finally {
-      Page<HmacKey.HmacKeyMetadata> metadatas =
-          storage.listHmacKeys(Storage.ListHmacKeysOption.serviceAccount(serviceAccount));
-      for (HmacKey.HmacKeyMetadata hmacKeyMetadata : metadatas.iterateAll()) {
-        storage.updateHmacKeyState(hmacKeyMetadata, HmacKey.HmacKeyState.INACTIVE);
+      cleanUpHmacKeys(serviceAccount);
+    }
+  }
+
+  private void cleanUpHmacKeys(ServiceAccount serviceAccount) {
+    Page<HmacKey.HmacKeyMetadata> metadatas =
+        storage.listHmacKeys(Storage.ListHmacKeysOption.serviceAccount(serviceAccount));
+    for (HmacKey.HmacKeyMetadata hmacKeyMetadata : metadatas.iterateAll()) {
+      if (hmacKeyMetadata.getState() == HmacKeyState.ACTIVE) {
+        hmacKeyMetadata = storage.updateHmacKeyState(hmacKeyMetadata, HmacKeyState.INACTIVE);
+      }
+      if (hmacKeyMetadata.getState() == HmacKeyState.INACTIVE) {
         storage.deleteHmacKey(hmacKeyMetadata);
       }
     }
@@ -3939,4 +3952,33 @@ public class ITStorageTest {
     blobGen2.downloadTo(actualData);
     assertEquals(contentGen2Expected, ByteBuffer.wrap(actualData.toByteArray()));
   }
+
+  @Test
+  public void testRpoConfig() {
+    String rpoBucket = RemoteStorageHelper.generateBucketName();
+    try {
+      Bucket bucket =
+          storage.create(
+              BucketInfo.newBuilder(rpoBucket).setLocation("NAM4").setRpo(Rpo.ASYNC_TURBO).build());
+      assertEquals("ASYNC_TURBO", bucket.getRpo().toString());
+
+      bucket.toBuilder().setRpo(Rpo.DEFAULT).build().update();
+
+      assertEquals("DEFAULT", storage.get(rpoBucket).getRpo().toString());
+    } finally {
+      storage.delete(rpoBucket);
+    }
+  }
+
+  private static String randString(Random rand, int length) {
+    final StringBuilder sb = new StringBuilder();
+    while (sb.length() < length) {
+      int i = rand.nextInt('z');
+      char c = (char) i;
+      if (Character.isLetter(c) || Character.isDigit(c)) {
+        sb.append(c);
+      }
+    }
+    return sb.toString();
+  }
 }