Repository: googleapis/java-storage
Pull request #1435 (2022-06-06 22:33:59)

feat: Bucket metadata grpc
Add remaining fields for Bucket metadata (defaultObjectAcls, Owner, Logging, Cors, and IamConfiguration) 
Fix OLM deleteRules encode/decode to only function when Rules are exactly what's expected otherwise return null.



Patch:
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 1bdacd01..989a7963 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
@@ -25,9 +25,12 @@ import static com.google.cloud.storage.Utils.todo;
 import com.google.cloud.storage.BlobInfo.CustomerEncryption;
 import com.google.cloud.storage.Conversions.Codec;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.io.BaseEncoding;
 import com.google.common.primitives.Ints;
 import com.google.protobuf.ByteString;
+import com.google.protobuf.Descriptors;
 import com.google.protobuf.Timestamp;
 import com.google.storage.v2.Bucket;
 import com.google.storage.v2.Bucket.Billing;
@@ -35,28 +38,32 @@ import com.google.storage.v2.HmacKeyMetadata;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
 import com.google.storage.v2.ObjectChecksums;
+import com.google.storage.v2.Owner;
 import com.google.type.Date;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
+import java.util.List;
 
 final class GrpcConversions {
   static final GrpcConversions INSTANCE = new GrpcConversions();
 
-  private final Codec<?, ?> entityCodec = Codec.of(Utils::todo, Utils::todo);
-  private final Codec<Acl, ObjectAccessControl> objectAclCodec = Codec.of(Utils::todo, Utils::todo);
+  private final Codec<Acl.Entity, String> entityCodec =
+      Codec.of(this::entityEncode, this::entityDecode);
+  private final Codec<Acl, ObjectAccessControl> objectAclCodec =
+      Codec.of(this::objectAclEncode, this::objectAclDecode);
   private final Codec<?, ?> bucketAclCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<HmacKey.HmacKeyMetadata, HmacKeyMetadata> hmacKeyMetadataCodec =
       Codec.of(this::hmacKeyMetadataEncode, this::hmacKeyMetadataDecode);
   private final Codec<?, ?> hmacKeyCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<ServiceAccount, com.google.storage.v2.ServiceAccount> serviceAccountCodec =
       Codec.of(this::serviceAccountEncode, this::serviceAccountDecode);
-  private final Codec<?, ?> corsCodec = Codec.of(Utils::todo, Utils::todo);
-  private final Codec<?, ?> loggingCodec = Codec.of(Utils::todo, Utils::todo);
-  private final Codec<?, ?> iamConfigurationCodec = Codec.of(Utils::todo, Utils::todo);
+  private final Codec<Cors, Bucket.Cors> corsCodec = Codec.of(this::corsEncode, this::corsDecode);
+  private final Codec<BucketInfo.Logging, Bucket.Logging> loggingCodec =
+      Codec.of(this::loggingEncode, this::loggingDecode);
+  private final Codec<BucketInfo.IamConfiguration, Bucket.IamConfig> iamConfigurationCodec =
+      Codec.of(this::iamConfigEncode, this::iamConfigDecode);
   private final Codec<BucketInfo.LifecycleRule, Bucket.Lifecycle.Rule> lifecycleRuleCodec =
       Codec.of(this::lifecycleRuleEncode, this::lifecycleRuleDecode);
   private final Codec<BucketInfo.DeleteRule, Bucket.Lifecycle.Rule> deleteRuleCodec =
@@ -105,8 +112,8 @@ final class GrpcConversions {
 
   private GrpcConversions() {}
 
-  Codec<?, ?> entity() {
-    return todo();
+  Codec<Acl.Entity, String> entity() {
+    return entityCodec;
   }
 
   Codec<Acl, ObjectAccessControl> objectAcl() {
@@ -129,16 +136,16 @@ final class GrpcConversions {
     return serviceAccountCodec;
   }
 
-  Codec<?, ?> cors() {
-    return todo();
+  Codec<Cors, Bucket.Cors> cors() {
+    return corsCodec;
   }
 
-  Codec<?, ?> logging() {
-    return todo();
+  Codec<BucketInfo.Logging, Bucket.Logging> logging() {
+    return loggingCodec;
   }
 
-  Codec<?, ?> iamConfiguration() {
-    return todo();
+  Codec<BucketInfo.IamConfiguration, Bucket.IamConfig> iamConfiguration() {
+    return iamConfigurationCodec;
   }
 
   Codec<BucketInfo.LifecycleRule, Bucket.Lifecycle.Rule> lifecycleRule() {
@@ -206,18 +213,20 @@ final class GrpcConversions {
         lift(Bucket.Lifecycle::getRuleList).andThen(toImmutableListOf(lifecycleRule()::decode)),
         to::setLifecycleRules);
     // preserve mapping to deprecated property
+    if (from.hasLifecycle()) {
+      to.setDeleteRules(
+          from.getLifecycle().getRuleList().stream()
+              .filter(this::isValidDeleteRule)
+              .map(deleteRule()::decode)
+              .collect(ImmutableList.toImmutableList()));
+    }
+    ifNonNull(from.getCorsList(), toImmutableListOf(cors()::decode), to::setCors);
+    ifNonNull(from.getLogging(), loggingCodec::decode, to::setLogging);
+    ifNonNull(from.getOwner(), lift(Owner::getEntity).andThen(this::entityDecode), to::setOwner);
     ifNonNull(
-        from.getLifecycle(),
-        lift(Bucket.Lifecycle::getRuleList).andThen(toImmutableListOf(deleteRule()::decode)),
-        to::setDeleteRules);
-    // TODO(frankyn): Add logging decoder
-    // TODO(frankyn): Add entity decoder
-    // TODO(frankyn): Add Cors decoder support
-    // TODO(frnakyn): Add DefaultObjectAcl decoder support
-    // TODO(frankyn): Add lifeycle decoder
-    // TODO(frankyn): Add deleteRules decoder
+        from.getDefaultObjectAclList(), toImmutableListOf(objectAcl()::decode), to::setDefaultAcl);
+    ifNonNull(from.getIamConfig(), iamConfiguration()::decode, to::setIamConfiguration);
     // TODO(frankyn): Add SelfLink when the field is available
-    // TODO(frankyn): Add decoder for iamConfig
     // TODO(frankyn): Add Etag when support is available
     return to.build();
   }
@@ -263,52 +272,179 @@ final class GrpcConversions {
     to.setVersioning(versioningBuilder.build());
     ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
     ifNonNull(from.getLabels(), to::putAllLabels);
-    // preserve mapping to deprecated property
-    Bucket.Lifecycle.Builder lifecycleBuilder = Bucket.Lifecycle.newBuilder();
+    to.setLifecycle(buildLifecyclePolicy(from));
+    ifNonNull(from.getLogging(), loggingCodec::encode, to::setLogging);
+    ifNonNull(from.getCors(), toImmutableListOf(cors()::encode), to::addAllCors);
     ifNonNull(
-        from.getLifecycleRules(),
-        Utils.toImmutableListOf(lifecycleRule()::encode),
-        lifecycleBuilder::addAllRule);
-    /*
-    // Corruption occurs by having deleteRule() values added
+        from.getOwner(),
+        lift(entity()::encode).andThen(o -> Owner.newBuilder().setEntity(o).build()),
+        to::setOwner);
     ifNonNull(
-       from.getDeleteRules(),
-       Utils.toImmutableListOf(deleteRule()::encode),
-       lifecycleBuilder::addAllRule);
-    */
-    to.setLifecycle(lifecycleBuilder.build());
-    // TODO(frankyn): Add logging decoder
-    // TODO(frankyn): Add entity decoder
-    // TODO(frankyn): Add Cors decoder support
-    // TODO(frnakyn): Add DefaultObjectAcl decoder support
-    // TODO(frankyn): Add lifeycle decoder
-    // TODO(frankyn): Add deleteRules decoder
+        from.getDefaultAcl(), toImmutableListOf(objectAcl()::encode), to::addAllDefaultObjectAcl);
+    ifNonNull(from.getIamConfiguration(), iamConfiguration()::encode, to::setIamConfig);
     // TODO(frankyn): Add SelfLink when the field is available
-    // TODO(frankyn): Add decoder for iamConfig
     // TODO(frankyn): Add Etag when support is avialable
     return to.build();
   }
 
+  private Bucket.Lifecycle buildLifecyclePolicy(BucketInfo from) {
+    // Handle duplicate rules introduced by deleteRules using a backing ImmutableSet
+    Bucket.Lifecycle.Builder lifecycleBuilder = Bucket.Lifecycle.newBuilder();
+    ImmutableSet.Builder<Bucket.Lifecycle.Rule> rules = new ImmutableSet.Builder<>();
+    if (from.getLifecycleRules() != null) {
+      rules.addAll(
+          from.getLifecycleRules().stream()
+              .map(lifecycleRule()::encode)
+              .collect(ImmutableSet.toImmutableSet()));
+    }
+    // preserve mapping to deprecated property
+    if (from.getDeleteRules() != null) {
+      rules.addAll(
+          from.getDeleteRules().stream()
+              .map(deleteRule()::encode)
+              .collect(ImmutableSet.toImmutableSet()));
+    }
+    return lifecycleBuilder.addAllRule(rules.build()).build();
+  }
+
+  private Bucket.Logging loggingEncode(BucketInfo.Logging from) {
+    Bucket.Logging.Builder to = Bucket.Logging.newBuilder();
+    if (from.getLogBucket() != null || from.getLogObjectPrefix() != null) {
+      to.setLogBucket(from.getLogBucket());
+      to.setLogObjectPrefix(from.getLogObjectPrefix());
+    }
+    return to.build();
+  }
+
+  private BucketInfo.Logging loggingDecode(Bucket.Logging from) {
+    return BucketInfo.Logging.newBuilder()
+        .setLogBucket(from.getLogBucket())
+        .setLogObjectPrefix(from.getLogObjectPrefix())
+        .build();
+  }
+
+  private Bucket.Cors corsEncode(Cors from) {
+    Bucket.Cors.Builder to = Bucket.Cors.newBuilder();
+    to.setMaxAgeSeconds(from.getMaxAgeSeconds());
+    to.addAllResponseHeader(from.getResponseHeaders());
+    ifNonNull(from.getMethods(), toImmutableListOf(java.lang.Object::toString), to::addAllMethod);
+    ifNonNull(from.getOrigins(), toImmutableListOf(java.lang.Object::toString), to::addAllOrigin);
+    return to.build();
+  }
+
+  private Cors corsDecode(Bucket.Cors from) {
+    Cors.Builder to = Cors.newBuilder().setMaxAgeSeconds(from.getMaxAgeSeconds());
+    ifNonNull(
+        from.getMethodList(),
+        m ->
+            m.stream()
+                .map(String::toUpperCase)
+                .map(HttpMethod::valueOf)
+                .collect(ImmutableList.toImmutableList()),
+        to::setMethods);
+    ifNonNull(from.getOriginList(), toImmutableListOf(Cors.Origin::of), to::setOrigins);
+    to.setResponseHeaders(from.getResponseHeaderList());
+    return to.build();
+  }
+
+  private String entityEncode(Acl.Entity from) {
+    if (from instanceof Acl.RawEntity) {
+      return from.getValue();
+    } else if (from instanceof Acl.User) {
+      switch (from.getValue()) {
+        case Acl.User.ALL_AUTHENTICATED_USERS:
+          return Acl.User.ALL_AUTHENTICATED_USERS;
+        case Acl.User.ALL_USERS:
+          return Acl.User.ALL_USERS;
+        default:
+          break;
+      }
+    }
+    // intentionally not an else so that if the default is hit above it will fall through to here
+    return from.getType().name().toLowerCase() + "-" + from.getValue();
+  }
+
+  private Acl.Entity entityDecode(String from) {
+    if (from.startsWith("user-")) {
+      return new Acl.User(from.substring(5));
+    }
+    if (from.equals(Acl.User.ALL_USERS)) {
+      return Acl.User.ofAllUsers();
+    }
+    if (from.equals(Acl.User.ALL_AUTHENTICATED_USERS)) {
+      return Acl.User.ofAllAuthenticatedUsers();
+    }
+    if (from.startsWith("group-")) {
+      return new Acl.Group(from.substring(6));
+    }
+    if (from.startsWith("domain-")) {
+      return new Acl.Domain(from.substring(7));
+    }
+    if (from.startsWith("project-")) {
+      int idx = from.indexOf('-', 8);
+      String team = from.substring(8, idx);
+      String projectId = from.substring(idx + 1);
+      return new Acl.Project(Acl.Project.ProjectRole.valueOf(team.toUpperCase()), projectId);
+    }
+    return new Acl.RawEntity(from);
+  }
+
+  private Acl objectAclDecode(ObjectAccessControl from) {
+    Acl.Role role = Acl.Role.valueOf(from.getRole());
+    Acl.Entity entity = entityDecode(from.getEntity());
+    // TODO: Add etag when it becomes available
+    return Acl.newBuilder(entity, role).setId(from.getId()).build();
+  }
+
+  private ObjectAccessControl objectAclEncode(Acl from) {
+    // TODO: Add etag when it becomes available
+    return ObjectAccessControl.newBuilder()
+        .setEntity(entityEncode(from.getEntity()))
+        .setRole(from.getRole().name())
+        .setId(from.getId())
+        .build();
+  }
+
+  private Bucket.IamConfig.UniformBucketLevelAccess ublaEncode(BucketInfo.IamConfiguration from) {
+    Bucket.IamConfig.UniformBucketLevelAccess.Builder to =
+        Bucket.IamConfig.UniformBucketLevelAccess.newBuilder();
+    to.setEnabled(from.isUniformBucketLevelAccessEnabled());
+    if (from.isUniformBucketLevelAccessEnabled() == Boolean.TRUE) {
+      ifNonNull(
+          from.getUniformBucketLevelAccessLockedTimeOffsetDateTime(),
+          timestampCodec::encode,
+          to::setLockTime);
+    }
+    return to.build();
+  }
+
+  private Bucket.IamConfig iamConfigEncode(BucketInfo.IamConfiguration from) {
+    Bucket.IamConfig.Builder to = Bucket.IamConfig.newBuilder();
+    to.setUniformBucketLevelAccess(ublaEncode(from));
+    if (from.getPublicAccessPrevention() != null) {
+      ifNonNull(from.getPublicAccessPrevention().getValue(), to::setPublicAccessPrevention);
+    }
+    return to.build();
+  }
+
+  private BucketInfo.IamConfiguration iamConfigDecode(Bucket.IamConfig from) {
+    Bucket.IamConfig.UniformBucketLevelAccess ubla = from.getUniformBucketLevelAccess();
+
+    BucketInfo.IamConfiguration.Builder to = BucketInfo.IamConfiguration.newBuilder();
+    ifNonNull(ubla.getEnabled(), to::setIsUniformBucketLevelAccessEnabled);
+    ifNonNull(
+        ubla.getLockTime(),
+        timestampCodec::decode,
+        to::setUniformBucketLevelAccessLockedTimeOffsetDateTime);
+    ifNonNull(
+        from.getPublicAccessPrevention(),
+        BucketInfo.PublicAccessPrevention::parse,
+        to::setPublicAccessPrevention);
+    return to.build();
+  }
+
   @SuppressWarnings("deprecation")
   private Bucket.Lifecycle.Rule deleteRuleEncode(BucketInfo.DeleteRule from) {
-    if (from instanceof BucketInfo.RawDeleteRule) {
-      Bucket.Lifecycle.Rule rule =
-          Bucket.Lifecycle.Rule.newBuilder()
-              .setAction(Bucket.Lifecycle.Rule.Action.newBuilder().setType(from.getType().name()))
-              .build();
-      String msg =
-          "The lifecycle condition "
-              + resolveRuleActionType(rule)
-              + " is not currently supported. Please update to the latest version of google-cloud-java."
-              + " Also, use LifecycleRule rather than the deprecated DeleteRule.";
-      // manually construct a log record, so we maintain class name and method name
-      // from the old implicit values.
-      LogRecord record = new LogRecord(Level.WARNING, msg);
-      record.setSourceClassName(BucketInfo.RawDeleteRule.class.getName());
-      record.setSourceMethodName("populateCondition");
-      BucketInfo.log.log(record);
-      return rule;
-    }
     Bucket.Lifecycle.Rule.Builder to = Bucket.Lifecycle.Rule.newBuilder();
     to.setAction(
         Bucket.Lifecycle.Rule.Action.newBuilder().setType(BucketInfo.DeleteRule.SUPPORTED_ACTION));
@@ -338,42 +474,42 @@ final class GrpcConversions {
     return to.build();
   }
 
-  private String resolveRuleActionType(Bucket.Lifecycle.Rule rule) {
-    if (rule != null && rule.getAction() != null) {
-      return rule.getAction().getType();
-    } else {
-      return null;
-    }
+  static final List<Descriptors.FieldDescriptor> SUPPORTED_CONDITIONS_DELETE_RULE =
+      ImmutableList.of(
+          Bucket.Lifecycle.Rule.Condition.getDescriptor()
+              .findFieldByNumber(Bucket.Lifecycle.Rule.Condition.AGE_DAYS_FIELD_NUMBER),
+          Bucket.Lifecycle.Rule.Condition.getDescriptor()
+              .findFieldByNumber(Bucket.Lifecycle.Rule.Condition.CREATED_BEFORE_FIELD_NUMBER),
+          Bucket.Lifecycle.Rule.Condition.getDescriptor()
+              .findFieldByNumber(Bucket.Lifecycle.Rule.Condition.NUM_NEWER_VERSIONS_FIELD_NUMBER),
+          Bucket.Lifecycle.Rule.Condition.getDescriptor()
+              .findFieldByNumber(Bucket.Lifecycle.Rule.Condition.IS_LIVE_FIELD_NUMBER));
+
+  private boolean isValidDeleteRule(Bucket.Lifecycle.Rule rule) {
+    return rule.hasAction()
+        && rule.getAction().getType().equals(BucketInfo.LifecycleRule.DeleteLifecycleAction.TYPE)
+        && rule.getCondition().getAllFields().keySet().size() == 1
+        && !rule.getCondition().getAllFields().keySet().stream()
+            .noneMatch(SUPPORTED_CONDITIONS_DELETE_RULE::contains);
   }
 
   @SuppressWarnings("deprecation")
   private BucketInfo.DeleteRule deleteRuleDecode(Bucket.Lifecycle.Rule from) {
-    if (from.getAction() != null
-        && BucketInfo.DeleteRule.SUPPORTED_ACTION.endsWith(resolveRuleActionType(from))) {
-      Bucket.Lifecycle.Rule.Condition condition = from.getCondition();
-      Integer age = condition.getAgeDays();
-      if (age != null) {
-        return new BucketInfo.AgeDeleteRule(age);
-      }
+    if (!isValidDeleteRule(from)) {
+      throw new IllegalArgumentException("Rule is not a valid DeleteRule" + from);
+    }
+    Bucket.Lifecycle.Rule.Condition condition = from.getCondition();
+    if (condition.hasAgeDays()) {
+      return new BucketInfo.AgeDeleteRule(condition.getAgeDays());
+    } else if (condition.hasCreatedBefore()) {
       Date date = condition.getCreatedBefore();
-      if (date != null) {
-        return new BucketInfo.CreatedBeforeDeleteRule(
-            OffsetDateTime.from(LocalDate.of(date.getYear(), date.getMonth(), date.getDay())));
-      }
-      Integer numNewerVersions = condition.getNumNewerVersions();
-      if (numNewerVersions != null) {
-        return new BucketInfo.NumNewerVersionsDeleteRule(numNewerVersions);
-      }
-      Boolean isLive = condition.getIsLive();
-      if (isLive != null) {
-        return new BucketInfo.IsLiveDeleteRule(isLive);
-      }
+      return new BucketInfo.CreatedBeforeDeleteRule(
+          OffsetDateTime.from(LocalDate.of(date.getYear(), date.getMonth(), date.getDay())));
+    } else if (condition.hasNumNewerVersions()) {
+      return new BucketInfo.NumNewerVersionsDeleteRule(condition.getNumNewerVersions());
+    } else {
+      return new BucketInfo.IsLiveDeleteRule(condition.getIsLive());
     }
-    return new BucketInfo.RawDeleteRule(
-        new com.google.api.services.storage.model.Bucket.Lifecycle.Rule()
-            .setAction(
-                new com.google.api.services.storage.model.Bucket.Lifecycle.Rule.Action()
-                    .setType(resolveRuleActionType(from))));
   }
 
   private Bucket.Lifecycle.Rule lifecycleRuleEncode(BucketInfo.LifecycleRule from) {
@@ -385,13 +521,22 @@ final class GrpcConversions {
 
   private Bucket.Lifecycle.Rule.Condition ruleConditionEncode(
       BucketInfo.LifecycleRule.LifecycleCondition from) {
-    Bucket.Lifecycle.Rule.Condition.Builder to =
-        Bucket.Lifecycle.Rule.Condition.newBuilder()
-            .setAgeDays(from.getAge())
-            .setIsLive(from.getIsLive())
-            .setNumNewerVersions(from.getNumberOfNewerVersions())
-            .setDaysSinceNoncurrentTime(from.getDaysSinceNoncurrentTime())
-            .setDaysSinceCustomTime(from.getDaysSinceCustomTime());
+    Bucket.Lifecycle.Rule.Condition.Builder to = Bucket.Lifecycle.Rule.Condition.newBuilder();
+    if (from.getAge() != null) {
+      to.setAgeDays(from.getAge());
+    }
+    if (from.getIsLive() != null) {
+      to.setIsLive(from.getIsLive());
+    }
+    if (from.getNumberOfNewerVersions() != null) {
+      to.setNumNewerVersions(from.getNumberOfNewerVersions());
+    }
+    if (from.getDaysSinceNoncurrentTime() != null) {
+      to.setDaysSinceNoncurrentTime(from.getDaysSinceNoncurrentTime());
+    }
+    if (from.getDaysSinceCustomTime() != null) {
+      to.setDaysSinceCustomTime(from.getDaysSinceCustomTime());
+    }
     ifNonNull(from.getCreatedBeforeOffsetDateTime(), odtDateCodec::encode, to::setCreatedBefore);
     ifNonNull(
         from.getNoncurrentTimeBeforeOffsetDateTime(),
@@ -447,21 +592,38 @@ final class GrpcConversions {
     Bucket.Lifecycle.Rule.Condition condition = from.getCondition();
 
     BucketInfo.LifecycleRule.LifecycleCondition.Builder conditionBuilder =
-        BucketInfo.LifecycleRule.LifecycleCondition.newBuilder()
-            .setAge(condition.getAgeDays())
-            .setCreateBeforeOffsetDateTime(odtDateCodec.decode(condition.getCreatedBefore()))
-            .setIsLive(condition.getIsLive())
-            .setNumberOfNewerVersions(condition.getNumNewerVersions())
-            .setDaysSinceNoncurrentTime(condition.getDaysSinceNoncurrentTime())
-            .setNoncurrentTimeBeforeOffsetDateTime(
-                odtDateCodec.decode(condition.getNoncurrentTimeBefore()))
-            .setCustomTimeBeforeOffsetDateTime(odtDateCodec.decode(condition.getCustomTimeBefore()))
-            .setDaysSinceCustomTime(condition.getDaysSinceCustomTime());
+        BucketInfo.LifecycleRule.LifecycleCondition.newBuilder();
+    if (condition.hasAgeDays()) {
+      conditionBuilder.setAge(condition.getAgeDays());
+    }
+    if (condition.hasCreatedBefore()) {
+      conditionBuilder.setCreateBeforeOffsetDateTime(
+          odtDateCodec.nullable().decode(condition.getCreatedBefore()));
+    }
+    if (condition.hasIsLive()) {
+      conditionBuilder.setIsLive(condition.getIsLive());
+    }
+    if (condition.hasNumNewerVersions()) {
+      conditionBuilder.setNumberOfNewerVersions(condition.getNumNewerVersions());
+    }
+    if (condition.hasDaysSinceNoncurrentTime()) {
+      conditionBuilder.setDaysSinceNoncurrentTime(condition.getDaysSinceNoncurrentTime());
+    }
+    if (condition.hasNoncurrentTimeBefore()) {
+      conditionBuilder.setNoncurrentTimeBeforeOffsetDateTime(
+          odtDateCodec.decode(condition.getNoncurrentTimeBefore()));
+    }
+    if (condition.hasCustomTimeBefore()) {
+      conditionBuilder.setCustomTimeBeforeOffsetDateTime(
+          odtDateCodec.decode(condition.getCustomTimeBefore()));
+    }
+    if (condition.hasDaysSinceCustomTime()) {
+      conditionBuilder.setDaysSinceCustomTime(condition.getDaysSinceCustomTime());
+    }
     ifNonNull(
         condition.getMatchesStorageClassList(),
         toImmutableListOf(StorageClass::valueOf),
         conditionBuilder::setMatchesStorageClass);
-
     return new BucketInfo.LifecycleRule(lifecycleAction, conditionBuilder.build());
   }
 


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/Crc32cUtilityPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/Crc32cUtilityPropertyTest.java
new file mode 100644
index 00000000..c5c86230
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/Crc32cUtilityPropertyTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 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.truth.Truth.assertThat;
+
+import com.google.common.hash.Hashing;
+import net.jqwik.api.ForAll;
+import net.jqwik.api.Property;
+
+public class Crc32cUtilityPropertyTest {
+  @Property
+  public void testCrc32cCombinePropertyTest(
+      @ForAll byte[] firstObject, @ForAll byte[] secondObject) {
+    int firstPartHash = Hashing.crc32c().hashBytes(firstObject).asInt();
+    int secondPartHash = Hashing.crc32c().hashBytes(secondObject).asInt();
+    int expected =
+        Hashing.crc32c().newHasher().putBytes(firstObject).putBytes(secondObject).hash().asInt();
+    int actual =
+        Crc32cUtility.crc32cCombineGoogle(firstPartHash, secondPartHash, secondObject.length);
+    assertThat(actual).isEqualTo(expected);
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
index a09fa567..d8e649de 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
@@ -55,9 +55,16 @@ public final class BucketArbitraryProvider implements ArbitraryProvider {
                         StorageArbitraries.buckets().encryption(),
                         StorageArbitraries.buckets().retentionPolicy(),
                         StorageArbitraries.buckets().lifecycle())
+                    .as(Tuple::of),
+                Combinators.combine(
+                        StorageArbitraries.buckets().logging(),
+                        StorageArbitraries.buckets().cors(),
+                        StorageArbitraries.buckets().objectAccessControl(),
+                        StorageArbitraries.buckets().owner(),
+                        StorageArbitraries.buckets().iamConfig())
                     .as(Tuple::of))
             .as(
-                (t1, t2) ->
+                (t1, t2, t3) ->
                     Bucket.newBuilder()
                         .setBucketId(t1.get1().get())
                         .setName(t1.get2().get())
@@ -75,6 +82,11 @@ public final class BucketArbitraryProvider implements ArbitraryProvider {
                         .setEncryption(t2.get6())
                         .setRetentionPolicy(t2.get7())
                         .setLifecycle(t2.get8())
+                        .setLogging(t3.get1())
+                        .addAllCors(t3.get2())
+                        .addAllDefaultObjectAcl(t3.get3())
+                        .setOwner(t3.get4())
+                        .setIamConfig(t3.get5())
                         .build());
     return Collections.singleton(as);
   }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
index 842cec59..4db0b213 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
@@ -27,9 +27,12 @@ import com.google.storage.v2.Bucket.RetentionPolicy;
 import com.google.storage.v2.Bucket.Versioning;
 import com.google.storage.v2.Bucket.Website;
 import com.google.storage.v2.CustomerEncryption;
+import com.google.storage.v2.ObjectAccessControl;
 import com.google.storage.v2.ObjectChecksums;
+import com.google.storage.v2.Owner;
 import com.google.type.Date;
 import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import net.jqwik.api.Arbitraries;
 import net.jqwik.api.Arbitrary;
@@ -163,36 +166,42 @@ public final class StorageArbitraries {
       Arbitrary<Date> conditionCustomTime = date();
       ListArbitrary<String> storageClassMatches = storageClass().list().uniqueElements();
 
-      return Combinators.combine(
-              actions(),
-              Combinators.combine(
-                      conditionIsLive,
-                      conditionAgeDays,
-                      conditionNumberOfNewVersions,
-                      conditionCreatedBeforeTime,
-                      conditionDaysSinceNoncurrentTime,
-                      conditionNoncurrentTime,
-                      conditionDaysSinceCustomTime,
-                      conditionCustomTime)
-                  .as(Tuple::of),
-              storageClassMatches)
-          .as(
-              (a, ct, s) ->
-                  Bucket.Lifecycle.Rule.newBuilder()
-                      .setAction(a)
-                      .setCondition(
-                          Bucket.Lifecycle.Rule.Condition.newBuilder()
-                              .setIsLive(ct.get1())
-                              .setAgeDays(ct.get2())
-                              .setNumNewerVersions(ct.get3())
-                              .setCreatedBefore(ct.get4())
-                              .setDaysSinceNoncurrentTime(ct.get5())
-                              .setNoncurrentTimeBefore(ct.get6())
-                              .setDaysSinceCustomTime(ct.get7())
-                              .setCustomTimeBefore(ct.get8())
-                              .addAllMatchesStorageClass(s)
-                              .build())
-                      .build());
+      return Arbitraries.oneOf(
+          Arbitraries.of(
+              Bucket.Lifecycle.Rule.newBuilder()
+                  .setAction(Bucket.Lifecycle.Rule.Action.newBuilder().setType("Delete").build())
+                  .setCondition(Bucket.Lifecycle.Rule.Condition.newBuilder().setAgeDays(10).build())
+                  .build()),
+          Combinators.combine(
+                  actions(),
+                  Combinators.combine(
+                          conditionIsLive,
+                          conditionAgeDays,
+                          conditionNumberOfNewVersions,
+                          conditionCreatedBeforeTime,
+                          conditionDaysSinceNoncurrentTime,
+                          conditionNoncurrentTime,
+                          conditionDaysSinceCustomTime,
+                          conditionCustomTime)
+                      .as(Tuple::of),
+                  storageClassMatches)
+              .as(
+                  (a, ct, s) ->
+                      Bucket.Lifecycle.Rule.newBuilder()
+                          .setAction(a)
+                          .setCondition(
+                              Bucket.Lifecycle.Rule.Condition.newBuilder()
+                                  .setIsLive(ct.get1())
+                                  .setAgeDays(ct.get2())
+                                  .setNumNewerVersions(ct.get3())
+                                  .setCreatedBefore(ct.get4())
+                                  .setDaysSinceNoncurrentTime(ct.get5())
+                                  .setNoncurrentTimeBefore(ct.get6())
+                                  .setDaysSinceCustomTime(ct.get7())
+                                  .setCustomTimeBefore(ct.get8())
+                                  .addAllMatchesStorageClass(s)
+                                  .build())
+                          .build()));
     }
 
     public Arbitrary<Bucket.Lifecycle> lifecycle() {
@@ -200,6 +209,7 @@ public final class StorageArbitraries {
           .list()
           .ofMinSize(0)
           .ofMaxSize(100)
+          .uniqueElements()
           .map((r) -> Bucket.Lifecycle.newBuilder().addAllRule(r).build());
     }
 
@@ -210,10 +220,85 @@ public final class StorageArbitraries {
           .as((i, n) -> Website.newBuilder().setMainPageSuffix(i).setNotFoundPage(n).build());
     }
 
+    public Arbitrary<Bucket.Logging> logging() {
+      Arbitrary<BucketName> loggingBucketName = StorageArbitraries.bucketName();
+      Arbitrary<String> loggingPrefix = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(10);
+      return Combinators.combine(loggingBucketName, loggingPrefix)
+          .as(
+              (b, p) ->
+                  Bucket.Logging.newBuilder().setLogBucket(b.get()).setLogObjectPrefix(p).build());
+    }
+
+    public ListArbitrary<Bucket.Cors> cors() {
+      Arbitrary<Integer> maxAgeSeconds =
+          Arbitraries.integers().between(0, OffsetDateTime.MAX.getSecond());
+      ListArbitrary<String> methods =
+          Arbitraries.of("GET", "DELETE", "UPDATE", "PATCH").list().uniqueElements();
+      ListArbitrary<String> responseHeaders =
+          Arbitraries.of("Content-Type", "Origin").list().uniqueElements();
+      ListArbitrary<String> origins = Arbitraries.of("*", "google.com").list().uniqueElements();
+      return Combinators.combine(methods, responseHeaders, origins, maxAgeSeconds)
+          .as(
+              (m, r, o, a) -> {
+                return Bucket.Cors.newBuilder()
+                    .addAllMethod(m)
+                    .addAllResponseHeader(r)
+                    .addAllOrigin(o)
+                    .setMaxAgeSeconds(a)
+                    .build();
+              })
+          .list()
+          .ofMinSize(0)
+          .ofMaxSize(10);
+    }
+
     public Arbitrary<Bucket.Billing> billing() {
       return bool().map(b -> Billing.newBuilder().setRequesterPays(b).build());
     }
 
+    public ListArbitrary<ObjectAccessControl> objectAccessControl() {
+      Arbitrary<String> entity = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
+      Arbitrary<String> role = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
+      return Combinators.combine(entity, role)
+          .as((e, r) -> ObjectAccessControl.newBuilder().setEntity(e).setRole(r).build())
+          .list();
+    }
+
+    public Arbitrary<Owner> owner() {
+      Arbitrary<String> entity = Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(1024);
+      return entity.map(e -> Owner.newBuilder().setEntity(e).build());
+    }
+
+    public Arbitrary<Bucket.IamConfig.UniformBucketLevelAccess> uniformBucketLevelAccess() {
+      return Combinators.combine(bool(), timestamp())
+          .as(
+              (e, l) -> {
+                Bucket.IamConfig.UniformBucketLevelAccess.Builder ublaBuilder =
+                    Bucket.IamConfig.UniformBucketLevelAccess.newBuilder();
+                ublaBuilder.setEnabled(e);
+                if (e) {
+                  ublaBuilder.setLockTime(l);
+                }
+                return ublaBuilder.build();
+              });
+    }
+
+    public Arbitrary<Bucket.IamConfig> iamConfig() {
+      Arbitrary<Bucket.IamConfig.UniformBucketLevelAccess> uniformBucketLevelAccess =
+          uniformBucketLevelAccess();
+      Arbitrary<String> pap = Arbitraries.of("enforced", "inherited");
+      return Combinators.combine(pap, uniformBucketLevelAccess())
+          .as(
+              (p, u) -> {
+                Bucket.IamConfig.Builder iamConfigBuilder = Bucket.IamConfig.newBuilder();
+                iamConfigBuilder.setUniformBucketLevelAccess(u);
+                if (u.getEnabled()) {
+                  iamConfigBuilder.setPublicAccessPrevention(p);
+                }
+                return iamConfigBuilder.build();
+              });
+    }
+
     public Arbitrary<Bucket.Encryption> encryption() {
       return Arbitraries.strings()
           .all()