Repository: googleapis/java-storage (java)

Issue:
Expose timeStorageClassUpdated property of the blob's
Add support of `timeStorageClassUpdated ` property of the [blob's](https://github.com/googleapis/google-api-java-client-services/blob/b39bb16a44aa99647d56b39054817ee6493ef8f5/clients/google-api-services-storage/v1/1.30.1/com/google/api/services/storage/model/StorageObject.java#L273). The purpose of exposing these field's is to get the time at which the object's storage class was last changed.

see the api doc : https://cloud.google.com/storage/docs/json_api/v1/objects#resource

~//cc: @frankyn 

Pull request:
feat: expose timeStorageClassUpdated property of blob's
Fixes #455 


Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
index 398eb316..6faef6c0 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
@@ -410,6 +410,12 @@ public class Blob extends BlobInfo {
       return this;
     }
 
+    @Override
+    public Builder setTimeStorageClassUpdated(Long timeStorageClassUpdated) {
+      infoBuilder.setTimeStorageClassUpdated(timeStorageClassUpdated);
+      return this;
+    }
+
     @Override
     Builder setMetageneration(Long metageneration) {
       infoBuilder.setMetageneration(metageneration);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
index 67256d1c..835e0d45 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobInfo.java
@@ -96,6 +96,7 @@ public class BlobInfo implements Serializable {
   private final String contentDisposition;
   private final String contentLanguage;
   private final StorageClass storageClass;
+  private final Long timeStorageClassUpdated;
   private final Integer componentCount;
   private final boolean isDirectory;
   private final CustomerEncryption customerEncryption;
@@ -297,6 +298,16 @@ public class BlobInfo implements Serializable {
     /** Sets the blob's storage class. */
     public abstract Builder setStorageClass(StorageClass storageClass);
 
+    /**
+     * Sets the modification time of an object's storage class. Once set it can't be unset directly,
+     * the only way is to rewrite the object with the desired storage class.
+     */
+    public Builder setTimeStorageClassUpdated(Long timeStorageClassUpdated) {
+      throw new UnsupportedOperationException(
+          "Override setTimeStorageClassUpdated with your own implementation,"
+              + " or use com.google.cloud.storage.Blob.");
+    }
+
     /** Sets the blob's user provided metadata. */
     public abstract Builder setMetadata(Map<String, String> metadata);
 
@@ -356,6 +367,7 @@ public class BlobInfo implements Serializable {
     private Boolean isDirectory;
     private CustomerEncryption customerEncryption;
     private StorageClass storageClass;
+    private Long timeStorageClassUpdated;
     private String kmsKeyName;
     private Boolean eventBasedHold;
     private Boolean temporaryHold;
@@ -391,6 +403,7 @@ public class BlobInfo implements Serializable {
       createTime = blobInfo.createTime;
       isDirectory = blobInfo.isDirectory;
       storageClass = blobInfo.storageClass;
+      timeStorageClassUpdated = blobInfo.timeStorageClassUpdated;
       kmsKeyName = blobInfo.kmsKeyName;
       eventBasedHold = blobInfo.eventBasedHold;
       temporaryHold = blobInfo.temporaryHold;
@@ -564,6 +577,12 @@ public class BlobInfo implements Serializable {
       return this;
     }
 
+    @Override
+    public Builder setTimeStorageClassUpdated(Long timeStorageClassUpdated) {
+      this.timeStorageClassUpdated = timeStorageClassUpdated;
+      return this;
+    }
+
     @Override
     Builder setMetageneration(Long metageneration) {
       this.metageneration = metageneration;
@@ -657,6 +676,7 @@ public class BlobInfo implements Serializable {
     createTime = builder.createTime;
     isDirectory = firstNonNull(builder.isDirectory, Boolean.FALSE);
     storageClass = builder.storageClass;
+    timeStorageClassUpdated = builder.timeStorageClassUpdated;
     kmsKeyName = builder.kmsKeyName;
     eventBasedHold = builder.eventBasedHold;
     temporaryHold = builder.temporaryHold;
@@ -917,6 +937,14 @@ public class BlobInfo implements Serializable {
     return storageClass;
   }
 
+  /**
+   * Returns the time that the object's storage class was last changed or the time of the object
+   * creation.
+   */
+  public Long getTimeStorageClassUpdated() {
+    return timeStorageClassUpdated;
+  }
+
   /** Returns the Cloud KMS key used to encrypt the blob, if any. */
   public String getKmsKeyName() {
     return kmsKeyName;
@@ -1049,6 +1077,9 @@ public class BlobInfo implements Serializable {
     if (storageClass != null) {
       storageObject.setStorageClass(storageClass.toString());
     }
+    if (timeStorageClassUpdated != null) {
+      storageObject.setTimeStorageClassUpdated(new DateTime(timeStorageClassUpdated));
+    }
 
     Map<String, String> pbMetadata = metadata;
     if (metadata != null && !Data.isNull(metadata)) {
@@ -1193,6 +1224,9 @@ public class BlobInfo implements Serializable {
     if (storageObject.getStorageClass() != null) {
       builder.setStorageClass(StorageClass.valueOf(storageObject.getStorageClass()));
     }
+    if (storageObject.getTimeStorageClassUpdated() != null) {
+      builder.setTimeStorageClassUpdated(storageObject.getTimeStorageClassUpdated().getValue());
+    }
     if (storageObject.getKmsKeyName() != null) {
       builder.setKmsKeyName(storageObject.getKmsKeyName());
     }
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 83ca9dd4..08eec26a 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
@@ -154,7 +154,8 @@ public interface Storage extends Service<StorageOptions> {
     TEMPORARY_HOLD("temporaryHold"),
     RETENTION_EXPIRATION_TIME("retentionExpirationTime"),
     UPDATED("updated"),
-    CUSTOM_TIME("customTime");
+    CUSTOM_TIME("customTime"),
+    TIME_STORAGE_CLASS_UPDATED("timeStorageClassUpdated");
 
     static final List<? extends FieldSelector> REQUIRED_FIELDS = ImmutableList.of(BUCKET, NAME);
 


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
index d4119971..d23e0dc7 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobInfoTest.java
@@ -75,6 +75,7 @@ public class BlobInfoTest {
   private static final String KMS_KEY_NAME =
       "projects/p/locations/kr-loc/keyRings/kr/cryptoKeys/key";
   private static final StorageClass STORAGE_CLASS = StorageClass.COLDLINE;
+  private static final Long TIME_STORAGE_CLASS_UPDATED = CREATE_TIME;
   private static final Boolean EVENT_BASED_HOLD = true;
   private static final Boolean TEMPORARY_HOLD = true;
   private static final Long RETENTION_EXPIRATION_TIME = 10L;
@@ -104,6 +105,7 @@ public class BlobInfoTest {
           .setCreateTime(CREATE_TIME)
           .setCustomTime(CUSTOM_TIME)
           .setStorageClass(STORAGE_CLASS)
+          .setTimeStorageClassUpdated(TIME_STORAGE_CLASS_UPDATED)
           .setKmsKeyName(KMS_KEY_NAME)
           .setEventBasedHold(EVENT_BASED_HOLD)
           .setTemporaryHold(TEMPORARY_HOLD)
@@ -199,6 +201,7 @@ public class BlobInfoTest {
     assertEquals(CREATE_TIME, BLOB_INFO.getCreateTime());
     assertEquals(CUSTOM_TIME, BLOB_INFO.getCustomTime());
     assertEquals(STORAGE_CLASS, BLOB_INFO.getStorageClass());
+    assertEquals(TIME_STORAGE_CLASS_UPDATED, BLOB_INFO.getTimeStorageClassUpdated());
     assertEquals(KMS_KEY_NAME, BLOB_INFO.getKmsKeyName());
     assertEquals(EVENT_BASED_HOLD, BLOB_INFO.getEventBasedHold());
     assertEquals(TEMPORARY_HOLD, BLOB_INFO.getTemporaryHold());
@@ -264,6 +267,7 @@ public class BlobInfoTest {
     assertEquals(expected.getCustomTime(), value.getCustomTime());
     assertEquals(expected.getUpdateTime(), value.getUpdateTime());
     assertEquals(expected.getStorageClass(), value.getStorageClass());
+    assertEquals(expected.getTimeStorageClassUpdated(), value.getTimeStorageClassUpdated());
     assertEquals(expected.getKmsKeyName(), value.getKmsKeyName());
     assertEquals(expected.getEventBasedHold(), value.getEventBasedHold());
     assertEquals(expected.getTemporaryHold(), value.getTemporaryHold());
@@ -319,6 +323,7 @@ public class BlobInfoTest {
     assertEquals(0L, (long) blobInfo.getSize());
     assertNull(blobInfo.getUpdateTime());
     assertNull(blobInfo.getStorageClass());
+    assertNull(blobInfo.getTimeStorageClassUpdated());
     assertNull(blobInfo.getKmsKeyName());
     assertNull(blobInfo.getEventBasedHold());
     assertNull(blobInfo.getTemporaryHold());
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
index 2ff21b71..36752a4d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobTest.java
@@ -92,6 +92,8 @@ public class BlobTest {
   private static final Long UPDATE_TIME = DELETE_TIME - 1L;
   private static final Long CREATE_TIME = UPDATE_TIME - 1L;
   private static final Long CUSTOM_TIME = CREATE_TIME - 1L;
+  private static final StorageClass STORAGE_CLASS = StorageClass.COLDLINE;
+  private static final Long TIME_STORAGE_CLASS_UPDATED = CREATE_TIME;
   private static final String ENCRYPTION_ALGORITHM = "AES256";
   private static final String KEY_SHA256 = "keySha";
   private static final BlobInfo.CustomerEncryption CUSTOMER_ENCRYPTION =
@@ -124,6 +126,8 @@ public class BlobTest {
           .setUpdateTime(UPDATE_TIME)
           .setCreateTime(CREATE_TIME)
           .setCustomTime(CUSTOM_TIME)
+          .setStorageClass(STORAGE_CLASS)
+          .setTimeStorageClassUpdated(TIME_STORAGE_CLASS_UPDATED)
           .setCustomerEncryption(CUSTOMER_ENCRYPTION)
           .setKmsKeyName(KMS_KEY_NAME)
           .setEventBasedHold(EVENT_BASED_HOLD)
@@ -513,6 +517,8 @@ public class BlobTest {
             .setCrc32c(CRC32)
             .setCreateTime(CREATE_TIME)
             .setCustomTime(CUSTOM_TIME)
+            .setStorageClass(STORAGE_CLASS)
+            .setTimeStorageClassUpdated(TIME_STORAGE_CLASS_UPDATED)
             .setCustomerEncryption(CUSTOMER_ENCRYPTION)
             .setKmsKeyName(KMS_KEY_NAME)
             .setEventBasedHold(EVENT_BASED_HOLD)
@@ -543,6 +549,8 @@ public class BlobTest {
     assertEquals(CRC32_HEX_STRING, blob.getCrc32cToHexString());
     assertEquals(CREATE_TIME, blob.getCreateTime());
     assertEquals(CUSTOM_TIME, blob.getCustomTime());
+    assertEquals(STORAGE_CLASS, blob.getStorageClass());
+    assertEquals(TIME_STORAGE_CLASS_UPDATED, blob.getTimeStorageClassUpdated());
     assertEquals(CUSTOMER_ENCRYPTION, blob.getCustomerEncryption());
     assertEquals(KMS_KEY_NAME, blob.getKmsKeyName());
     assertEquals(EVENT_BASED_HOLD, blob.getEventBasedHold());
@@ -576,6 +584,8 @@ public class BlobTest {
     assertNull(blob.getCrc32c());
     assertNull(blob.getCrc32cToHexString());
     assertNull(blob.getCreateTime());
+    assertNull(blob.getStorageClass());
+    assertNull(blob.getTimeStorageClassUpdated());
     assertNull(blob.getCustomerEncryption());
     assertNull(blob.getKmsKeyName());
     assertNull(blob.getEventBasedHold());
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 925e3d6d..84936e0d 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
@@ -3567,4 +3567,42 @@ public class ITStorageTest {
       RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
     }
   }
+
+  @Test
+  public void testBlobTimeStorageClassUpdated() {
+    String blobName = "test-blob-with-storage-class";
+    StorageClass storageClass = StorageClass.COLDLINE;
+    BlobInfo blob = BlobInfo.newBuilder(BUCKET, blobName).setStorageClass(storageClass).build();
+    Blob remoteBlob = storage.create(blob);
+    assertThat(remoteBlob).isNotNull();
+    assertThat(remoteBlob.getBucket()).isEqualTo(blob.getBucket());
+    assertThat(remoteBlob.getName()).isEqualTo(blob.getName());
+    assertThat(remoteBlob.getCreateTime()).isNotNull();
+    assertThat(remoteBlob.getUpdateTime()).isEqualTo(remoteBlob.getCreateTime());
+    assertThat(remoteBlob.getTimeStorageClassUpdated()).isEqualTo(remoteBlob.getCreateTime());
+
+    // We can't change an object's storage class directly, the only way is to rewrite the object
+    // with the desired storage class.
+    BlobId blobId = BlobId.of(BUCKET, blobName);
+    Storage.CopyRequest request =
+        Storage.CopyRequest.newBuilder()
+            .setSource(blobId)
+            .setTarget(BlobInfo.newBuilder(blobId).setStorageClass(StorageClass.STANDARD).build())
+            .build();
+    Blob updatedBlob1 = storage.copy(request).getResult();
+    assertThat(updatedBlob1.getTimeStorageClassUpdated()).isNotNull();
+    assertThat(updatedBlob1.getCreateTime()).isGreaterThan(remoteBlob.getCreateTime());
+    assertThat(updatedBlob1.getUpdateTime()).isGreaterThan(remoteBlob.getCreateTime());
+    assertThat(updatedBlob1.getTimeStorageClassUpdated())
+        .isGreaterThan(remoteBlob.getTimeStorageClassUpdated());
+
+    // Updates the other properties of the blob's to check the difference between blob updateTime
+    // and timeStorageClassUpdated.
+    Blob updatedBlob2 = updatedBlob1.toBuilder().setContentType(CONTENT_TYPE).build().update();
+    assertThat(updatedBlob2.getUpdateTime())
+        .isGreaterThan(updatedBlob2.getTimeStorageClassUpdated());
+    assertThat(updatedBlob2.getTimeStorageClassUpdated())
+        .isEqualTo(updatedBlob1.getTimeStorageClassUpdated());
+    assertThat(updatedBlob2.delete()).isTrue();
+  }
 }