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 db2eba8366..7d25cde129 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 @@ -35,6 +35,7 @@ import java.io.InputStream; import java.io.ObjectInputStream; import java.io.Serializable; +import java.math.BigInteger; import java.security.Key; import java.time.Duration; import java.time.OffsetDateTime; @@ -502,7 +503,7 @@ public Builder setName(String name) { } @Override - Builder setProject(String project) { + Builder setProject(BigInteger project) { infoBuilder.setProject(project); return this; } 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 c9ade02303..3899c703a7 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 @@ -45,6 +45,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; +import java.math.BigInteger; import java.time.Duration; import java.time.OffsetDateTime; import java.util.ArrayList; @@ -80,7 +81,7 @@ public class BucketInfo implements Serializable { private static final long serialVersionUID = 4793572058456298945L; private final String generatedId; - private final String project; + private final BigInteger project; private final String name; private final Acl.Entity owner; private final String selfLink; @@ -1657,7 +1658,7 @@ public boolean isLive() { public abstract static class Builder { Builder() {} - abstract Builder setProject(String project); + abstract Builder setProject(BigInteger project); /** Sets the bucket's name. */ public abstract Builder setName(String name); @@ -1923,7 +1924,7 @@ public Builder setRetentionPeriodDuration(Duration retentionPeriod) { static final class BuilderImpl extends Builder { private String generatedId; - private String project; + private BigInteger project; private String name; private Acl.Entity owner; private String selfLink; @@ -2007,7 +2008,10 @@ public Builder setName(String name) { } @Override - Builder setProject(String project) { + Builder setProject(BigInteger project) { + if (!Objects.equals(this.project, project)) { + modifiedFields.add(BucketField.PROJECT); + } this.project = project; return this; } @@ -2637,7 +2641,8 @@ private Builder clearDeleteLifecycleRules() { modifiedFields = builder.modifiedFields.build(); } - String getProject() { + /** The project number of the project the bucket belongs to */ + public BigInteger getProject() { return project; } @@ -2993,6 +2998,7 @@ public Builder toBuilder() { public int hashCode() { return Objects.hash( generatedId, + project, name, owner, selfLink, @@ -3022,6 +3028,7 @@ public int hashCode() { locationType, objectRetention, softDeletePolicy, + customPlacementConfig, hierarchicalNamespace, logging); } @@ -3036,6 +3043,7 @@ public boolean equals(Object o) { } BucketInfo that = (BucketInfo) o; return Objects.equals(generatedId, that.generatedId) + && Objects.equals(project, that.project) && Objects.equals(name, that.name) && Objects.equals(owner, that.owner) && Objects.equals(selfLink, that.selfLink) @@ -3063,6 +3071,7 @@ public boolean equals(Object o) { && Objects.equals(iamConfiguration, that.iamConfiguration) && Objects.equals(autoclass, that.autoclass) && Objects.equals(locationType, that.locationType) + && Objects.equals(customPlacementConfig, that.customPlacementConfig) && Objects.equals(objectRetention, that.objectRetention) && Objects.equals(softDeletePolicy, that.softDeletePolicy) && Objects.equals(hierarchicalNamespace, that.hierarchicalNamespace) 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 39d5986db6..4d9d5e31d2 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 @@ -20,7 +20,7 @@ import static com.google.cloud.storage.Utils.bucketNameCodec; import static com.google.cloud.storage.Utils.ifNonNull; import static com.google.cloud.storage.Utils.lift; -import static com.google.cloud.storage.Utils.projectNameCodec; +import static com.google.cloud.storage.Utils.projectNumberResourceCodec; import com.google.api.pathtemplate.PathTemplate; import com.google.cloud.Binding; @@ -204,7 +204,9 @@ Codec policyCodec() { private BucketInfo bucketInfoDecode(Bucket from) { BucketInfo.Builder to = new BucketInfo.BuilderImpl(bucketNameCodec.decode(from.getName())); - to.setProject(projectNameCodec.decode(from.getProject())); + if (!from.getProject().isEmpty()) { + to.setProject(projectNumberResourceCodec.decode(from.getProject())); + } to.setGeneratedId(from.getBucketId()); maybeDecodeRetentionPolicy(from, to); ifNonNull(from.getLocation(), to::setLocation); @@ -304,7 +306,7 @@ private BucketInfo bucketInfoDecode(Bucket from) { private Bucket bucketInfoEncode(BucketInfo from) { Bucket.Builder to = Bucket.newBuilder(); to.setName(bucketNameCodec.encode(from.getName())); - ifNonNull(from.getProject(), projectNameCodec::encode, to::setProject); + ifNonNull(from.getProject(), projectNumberResourceCodec::encode, to::setProject); ifNonNull(from.getGeneratedId(), to::setBucketId); maybeEncodeRetentionPolicy(from, to); ifNonNull(from.getLocation(), to::setLocation); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java index 8a838604ca..50b3596c99 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java @@ -26,6 +26,7 @@ import static com.google.cloud.storage.StorageV2ProtoUtils.objectAclEntityOrAltEq; import static com.google.cloud.storage.Utils.bucketNameCodec; import static com.google.cloud.storage.Utils.ifNonNull; +import static com.google.cloud.storage.Utils.projectNameCodec; import static com.google.common.base.MoreObjects.firstNonNull; import static java.util.Objects.requireNonNull; @@ -216,15 +217,15 @@ public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) { Opts opts = Opts.unwrap(options).resolveFrom(bucketInfo).prepend(defaultOpts); GrpcCallContext grpcCallContext = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault()); - if (bucketInfo.getProject() == null || bucketInfo.getProject().trim().isEmpty()) { - bucketInfo = bucketInfo.toBuilder().setProject(getOptions().getProjectId()).build(); - } com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo); CreateBucketRequest.Builder builder = CreateBucketRequest.newBuilder() .setBucket(bucket) .setBucketId(bucketInfo.getName()) .setParent("projects/_"); + if (bucketInfo.getProject() == null) { + builder.getBucketBuilder().setProject(projectNameCodec.encode(getOptions().getProjectId())); + } CreateBucketRequest req = opts.createBucketsRequest().apply(builder).build(); GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext()); return retrier.run( 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 12562cb020..7f9da1f588 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 @@ -22,7 +22,6 @@ import static com.google.cloud.storage.Utils.ifNonNull; import static com.google.cloud.storage.Utils.lift; import static com.google.cloud.storage.Utils.nullableDateTimeCodec; -import static com.google.cloud.storage.Utils.projectNameCodec; import static com.google.common.base.MoreObjects.firstNonNull; import com.google.api.client.util.Data; @@ -90,10 +89,6 @@ @InternalApi final class JsonConversions { static final JsonConversions INSTANCE = new JsonConversions(); - // gRPC has a Bucket.project property that apiary doesn't have yet. - // when converting from gRPC to apiary or vice-versa we want to preserve this property. Until - // such a time as the apiary model has a project field, we manually apply it with this name. - private static final String PROJECT_ID_FIELD_NAME = "x_project"; private final Codec entityCodec = Codec.of(this::entityEncode, this::entityDecode); @@ -394,7 +389,7 @@ private SoftDeletePolicy softDeletePolicyDecode(Bucket.SoftDeletePolicy from) { private Bucket bucketInfoEncode(BucketInfo from) { Bucket to = new Bucket(); - ifNonNull(from.getProject(), projectNameCodec::encode, p -> to.set(PROJECT_ID_FIELD_NAME, p)); + ifNonNull(from.getProject(), to::setProjectNumber); ifNonNull(from.getAcl(), toListOf(bucketAcl()::encode), to::setAcl); ifNonNull(from.getCors(), toListOf(cors()::encode), to::setCors); ifNonNull(from.getCreateTimeOffsetDateTime(), dateTimeCodec::encode, to::setTimeCreated); @@ -477,10 +472,7 @@ private Bucket bucketInfoEncode(BucketInfo from) { @SuppressWarnings("deprecation") private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket from) { BucketInfo.Builder to = new BucketInfo.BuilderImpl(from.getName()); - ifNonNull( - from.get(PROJECT_ID_FIELD_NAME), - lift(String.class::cast).andThen(projectNameCodec::decode), - to::setProject); + ifNonNull(from.getProjectNumber(), to::setProject); ifNonNull(from.getAcl(), toListOf(bucketAcl()::decode), to::setAcl); ifNonNull(from.getCors(), toListOf(cors()::decode), to::setCors); ifNonNull(from.getDefaultObjectAcl(), toListOf(objectAcl()::decode), to::setDefaultAcl); 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 301792e148..d2c38763e5 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 @@ -62,6 +62,7 @@ import java.io.ObjectInputStream; import java.io.OutputStream; import java.io.Serializable; +import java.math.BigInteger; import java.net.URL; import java.net.URLConnection; import java.nio.file.Path; @@ -190,7 +191,10 @@ enum BucketField implements FieldSelector, NamedField { SOFT_DELETE_POLICY( "softDeletePolicy", "soft_delete_policy", - com.google.api.services.storage.model.Bucket.SoftDeletePolicy.class); + com.google.api.services.storage.model.Bucket.SoftDeletePolicy.class), + + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + PROJECT("projectNumber", "project", BigInteger.class); static final List REQUIRED_FIELDS = ImmutableList.of(NAME); private static final Map JSON_FIELD_NAME_INDEX; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java index 0e436bd0dc..4bc73dacdd 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java @@ -25,6 +25,7 @@ import com.google.cloud.storage.Conversions.Codec; import com.google.cloud.storage.UnifiedOpts.NamedField; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.MapDifference; @@ -33,6 +34,7 @@ import com.google.common.primitives.Ints; import com.google.storage.v2.BucketName; import com.google.storage.v2.ProjectName; +import java.math.BigInteger; import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; @@ -174,6 +176,27 @@ final class Utils { } }); + /** + * Define a Codec which encapsulates the logic necessary to handle encoding and decoding project + * numbers. + */ + static final Codec<@NonNull BigInteger, @NonNull String> projectNumberResourceCodec = + Codec.of( + projectNumber -> { + requireNonNull(projectNumber, "projectNumber must be non null"); + return ProjectName.format(projectNumber.toString()); + }, + projectNumberResource -> { + requireNonNull(projectNumberResource, "projectNumberResource must be non null"); + Preconditions.checkArgument( + ProjectName.isParsableFrom(projectNumberResource), + "projectNumberResource '%s' is not parsable as a %s", + projectNumberResource, + ProjectName.class.getName()); + ProjectName parse = ProjectName.parse(projectNumberResource); + return new BigInteger(parse.getProject()); + }); + static final Codec crc32cCodec = Codec.of(Utils::crc32cEncode, Utils::crc32cDecode); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcUtilsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcUtilsTest.java index 06dfcca5bc..9ca5bcd1b7 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcUtilsTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcUtilsTest.java @@ -16,8 +16,14 @@ package com.google.cloud.storage; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.cloud.storage.Conversions.Codec; import java.io.IOException; +import java.math.BigInteger; import java.util.Collections; +import org.checkerframework.checker.nullness.qual.NonNull; import org.junit.Test; public final class GrpcUtilsTest { @@ -26,4 +32,41 @@ public final class GrpcUtilsTest { public void closeAll_noNpeIfNullStream() throws IOException { GrpcUtils.closeAll(Collections.singletonList(null)); } + + @Test + public void projectNumberResourceCodec_simple() { + Codec<@NonNull BigInteger, @NonNull String> codec = Utils.projectNumberResourceCodec; + + String encode = codec.encode(new BigInteger("34567892123")); + assertThat(encode).isEqualTo("projects/34567892123"); + + BigInteger decode = codec.decode(encode); + assertThat(decode).isEqualTo(new BigInteger("34567892123")); + } + + @Test + public void projectNumberResourceCodec_decode_illegalArgumentException_whenUnParsable() { + String bad = "not-a-projects/123081892932"; + IllegalArgumentException iae = + assertThrows( + IllegalArgumentException.class, () -> Utils.projectNumberResourceCodec.decode(bad)); + + assertThat(iae).hasMessageThat().contains(bad); + } + + @Test + public void projectNumberResourceCodec_decode_nonNull() { + assertThrows(NullPointerException.class, () -> Utils.projectNumberResourceCodec.decode(null)); + } + + @Test + public void projectNumberResourceCodec_encode_nonNull() { + assertThrows(NullPointerException.class, () -> Utils.projectNumberResourceCodec.encode(null)); + } + + @Test + public void projectNumberResourceCodec_decode_notProjectNumber() { + String bad = "projects/not-a-number"; + assertThrows(NumberFormatException.class, () -> Utils.projectNumberResourceCodec.decode(bad)); + } } 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 c4c3c40591..36991f1b83 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 @@ -98,6 +98,7 @@ public static final class BucketReadMaskTestParameters implements ParametersProv public ImmutableList parameters() { ImmutableList> args = ImmutableList.of( + new Args<>(BucketField.PROJECT, LazyAssertion.equal()), new Args<>(BucketField.ACL, LazyAssertion.equal()), new Args<>(BucketField.AUTOCLASS, LazyAssertion.equal()), new Args<>(BucketField.BILLING, LazyAssertion.equal()), 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 130bd0e11a..905fe5478c 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 @@ -328,7 +328,7 @@ public void storage_BucketGetOption_fields_BucketField() { "website", "softDeletePolicy", "hierarchicalNamespace", - "website"); + "projectNumber"); s.get( b.getName(), BucketGetOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values()))); @@ -821,7 +821,7 @@ public void storage_BucketListOption_fields_BucketField() { "items/website", "items/softDeletePolicy", "items/hierarchicalNamespace", - "items/website"); + "items/projectNumber"); s.list(BucketListOption.fields(TestUtils.filterOutHttpOnlyBucketFields(BucketField.values()))); requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet()); } 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 c5d361ea34..1839c32b4f 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 @@ -18,7 +18,7 @@ import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull; -import com.google.cloud.storage.jqwik.StorageArbitraries.ProjectID; +import com.google.cloud.storage.jqwik.StorageArbitraries.ProjectNumber; import com.google.storage.v2.Bucket; import com.google.storage.v2.BucketName; import com.google.storage.v2.ProjectName; @@ -76,7 +76,7 @@ public Set> provideFor(TypeUsage targetType, SubtypeProvider subtyp StorageArbitraries.etag()) .as(Tuple::of), Combinators.combine( - StorageArbitraries.projectID().map(ProjectID::toProjectName), + StorageArbitraries.projectNumber().map(ProjectNumber::toProjectName), StorageArbitraries .alnum() // ignored for now, tuples can't be a single element ) 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 8750a2932c..f5ee93ce0e 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 @@ -138,6 +138,10 @@ public static Arbitrary projectID() { .as((first, mid, last) -> new ProjectID(first + mid + last)); } + public static Arbitrary projectNumber() { + return Arbitraries.bigIntegers().greaterOrEqual(BigInteger.ONE).map(ProjectNumber::new); + } + public static Arbitrary kmsKey() { return Arbitraries.of("kms-key1", "kms-key2").injectNull(0.75); } @@ -444,6 +448,23 @@ public ProjectName toProjectName() { } } + public static final class ProjectNumber { + + private final BigInteger value; + + private ProjectNumber(BigInteger value) { + this.value = value; + } + + public BigInteger get() { + return value; + } + + public ProjectName toProjectName() { + return ProjectName.of(value.toString()); + } + } + public static Objects objects() { return Objects.INSTANCE; }