From f45abc480c630e6ff50d8be9ba2cf9b2b070c3ab Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Thu, 21 Aug 2025 10:56:04 +0530 Subject: [PATCH 1/2] feat: add Object Contexts --- .../clirr-ignored-differences.xml | 6 + .../java/com/google/cloud/storage/Blob.java | 12 ++ .../com/google/cloud/storage/BlobInfo.java | 194 +++++++++++++++++- .../google/cloud/storage/GrpcConversions.java | 57 +++++ .../google/cloud/storage/JsonConversions.java | 54 +++++ .../com/google/cloud/storage/Storage.java | 62 +++++- .../com/google/cloud/storage/UnifiedOpts.java | 17 ++ .../cloud/storage/spi/v1/HttpStorageRpc.java | 3 +- .../cloud/storage/spi/v1/StorageRpc.java | 1 + .../google/cloud/storage/BlobInfoTest.java | 10 + .../com/google/cloud/storage/BlobTest.java | 10 + .../cloud/storage/it/ITBlobReadMaskTest.java | 1 + .../storage/it/ITOptionRegressionTest.java | 6 +- .../jqwik/ObjectArbitraryProvider.java | 4 +- .../storage/jqwik/StorageArbitraries.java | 31 +++ 15 files changed, 461 insertions(+), 7 deletions(-) diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml index 9cb223aebc..7a61b49855 100644 --- a/google-cloud-storage/clirr-ignored-differences.xml +++ b/google-cloud-storage/clirr-ignored-differences.xml @@ -21,6 +21,12 @@ com.google.cloud.storage.BucketInfo$Builder setHierarchicalNamespace(com.google.cloud.storage.BucketInfo$HierarchicalNamespace) + + 7013 + com/google/cloud/storage/BlobInfo$Builder + com.google.cloud.storage.BlobInfo$Builder setContexts(com.google.cloud.storage.BlobInfo$ObjectContexts) + + 7013 com/google/cloud/storage/BlobInfo$Builder 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 8a6c3d1b7f..03d9d3f1cb 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 @@ -550,6 +550,12 @@ public Builder setRetention(Retention retention) { return this; } + @Override + public Builder setContexts(ObjectContexts contexts) { + infoBuilder.setContexts(contexts); + return this; + } + @Override public Blob build() { return new Blob(storage, infoBuilder); @@ -739,6 +745,12 @@ Builder clearRetentionExpirationTime() { infoBuilder.clearRetentionExpirationTime(); return this; } + + @Override + Builder clearContexts() { + infoBuilder.clearContexts(); + return this; + } } Blob(Storage storage, BlobInfo.BuilderImpl infoBuilder) { 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 67324b197b..c6c769e009 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 @@ -31,6 +31,7 @@ import com.google.cloud.storage.UnifiedOpts.NamedField; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.BaseEncoding; import java.io.Serializable; @@ -112,6 +113,7 @@ public class BlobInfo implements Serializable { private final Retention retention; private final OffsetDateTime softDeleteTime; private final OffsetDateTime hardDeleteTime; + private ObjectContexts contexts; private final transient ImmutableSet modifiedFields; /** This class is meant for internal use only. Users are discouraged from using this class. */ @@ -289,6 +291,167 @@ public static Mode[] values() { } } + public static final class ObjectContexts implements Serializable { + + private static final long serialVersionUID = -5993852233545224424L; + + private final ImmutableMap custom; + + private ObjectContexts(Builder builder) { + this.custom = builder.custom; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder().setCustom(this.custom); + } + + /** Returns the map of user-defined object contexts. */ + public Map getCustom() { + return custom; + } + + @Override + public int hashCode() { + return Objects.hash(custom); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ObjectContexts other = (ObjectContexts) obj; + return Objects.equals(this.custom, other.custom); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("custom", custom).toString(); + } + + public static final class Builder { + + private ImmutableMap custom; + + private Builder() {} + + public Builder setCustom(Map custom) { + this.custom = custom == null ? ImmutableMap.of() : ImmutableMap.copyOf(custom); + return this; + } + + public ObjectContexts build() { + return new ObjectContexts(this); + } + } + } + + /** Represents the payload of a user-defined object context. */ + public static final class ObjectCustomContextPayload implements Serializable { + + private static final long serialVersionUID = 557621132294323214L; + + private final String value; + private final OffsetDateTime createTime; + private final OffsetDateTime updateTime; + + private ObjectCustomContextPayload(Builder builder) { + this.value = builder.value; + this.createTime = builder.createTime; + this.updateTime = builder.updateTime; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder() + .setValue(this.value) + .setCreateTime(this.createTime) + .setUpdateTime(this.updateTime); + } + + public String getValue() { + return value; + } + + public OffsetDateTime getCreateTime() { + return createTime; + } + + public OffsetDateTime getUpdateTime() { + return updateTime; + } + + @Override + public int hashCode() { + return Objects.hash(value, createTime, updateTime); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ObjectCustomContextPayload other = (ObjectCustomContextPayload) obj; + return Objects.equals(value, other.value) + && Objects.equals(createTime, other.createTime) + && Objects.equals(updateTime, other.updateTime); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .add("createTime", createTime) + .add("updateTime", updateTime) + .toString(); + } + + public static final class Builder { + + private String value; + private OffsetDateTime createTime; + private OffsetDateTime updateTime; + + private Builder() {} + + public Builder(String value) { + setValue(value); + } + + public Builder setValue(String value) { + this.value = value; + return this; + } + + public Builder setCreateTime(OffsetDateTime createTime) { + this.createTime = createTime; + return this; + } + + public Builder setUpdateTime(OffsetDateTime updateTime) { + this.updateTime = updateTime; + return this; + } + + public ObjectCustomContextPayload build() { + return new ObjectCustomContextPayload(this); + } + } + } + /** Builder for {@code BlobInfo}. */ public abstract static class Builder { @@ -543,6 +706,8 @@ Builder setRetentionExpirationTimeOffsetDateTime(OffsetDateTime retentionExpirat public abstract Builder setRetention(Retention retention); + public abstract Builder setContexts(ObjectContexts contexts); + /** Creates a {@code BlobInfo} object. */ public abstract BlobInfo build(); @@ -607,6 +772,8 @@ Builder setRetentionExpirationTimeOffsetDateTime(OffsetDateTime retentionExpirat abstract Builder clearTemporaryHold(); abstract Builder clearRetentionExpirationTime(); + + abstract Builder clearContexts(); } static final class BuilderImpl extends Builder { @@ -644,6 +811,7 @@ static final class BuilderImpl extends Builder { private Retention retention; private OffsetDateTime softDeleteTime; private OffsetDateTime hardDeleteTime; + private ObjectContexts contexts; private final ImmutableSet.Builder modifiedFields = ImmutableSet.builder(); BuilderImpl(BlobId blobId) { @@ -684,6 +852,7 @@ static final class BuilderImpl extends Builder { retention = blobInfo.retention; softDeleteTime = blobInfo.softDeleteTime; hardDeleteTime = blobInfo.hardDeleteTime; + contexts = blobInfo.contexts; } @Override @@ -1095,6 +1264,13 @@ public Builder setRetention(Retention retention) { return this; } + @Override + public Builder setContexts(ObjectContexts contexts) { + modifiedFields.add(BlobField.OBJECT_CONTEXTS); + this.contexts = contexts; + return this; + } + @Override public BlobInfo build() { checkNotNull(blobId); @@ -1285,6 +1461,12 @@ Builder clearRetentionExpirationTime() { this.retentionExpirationTime = null; return this; } + + @Override + Builder clearContexts() { + this.contexts = null; + return this; + } } BlobInfo(BuilderImpl builder) { @@ -1321,6 +1503,7 @@ Builder clearRetentionExpirationTime() { retention = builder.retention; softDeleteTime = builder.softDeleteTime; hardDeleteTime = builder.hardDeleteTime; + contexts = builder.contexts; modifiedFields = builder.modifiedFields.build(); } @@ -1731,6 +1914,10 @@ public Retention getRetention() { return retention; } + public ObjectContexts getContexts() { + return contexts; + } + /** Returns a builder for the current blob. */ public Builder toBuilder() { return new BuilderImpl(this); @@ -1745,6 +1932,7 @@ public String toString() { .add("size", getSize()) .add("content-type", getContentType()) .add("metadata", getMetadata()) + .add("contexts", getContexts()) .toString(); } @@ -1783,7 +1971,8 @@ public int hashCode() { retention, retentionExpirationTime, softDeleteTime, - hardDeleteTime); + hardDeleteTime, + contexts); } @Override @@ -1827,7 +2016,8 @@ public boolean equals(Object o) { && Objects.equals(retentionExpirationTime, blobInfo.retentionExpirationTime) && Objects.equals(retention, blobInfo.retention) && Objects.equals(softDeleteTime, blobInfo.softDeleteTime) - && Objects.equals(hardDeleteTime, blobInfo.hardDeleteTime); + && Objects.equals(hardDeleteTime, blobInfo.hardDeleteTime) + && Objects.equals(contexts, blobInfo.contexts); } ImmutableSet getModifiedFields() { 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 a8354e6b42..9310005961 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 @@ -44,6 +44,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; import com.google.common.io.BaseEncoding; import com.google.protobuf.ByteString; import com.google.protobuf.ProtocolStringList; @@ -59,6 +60,8 @@ import com.google.storage.v2.Object; import com.google.storage.v2.ObjectAccessControl; import com.google.storage.v2.ObjectChecksums; +import com.google.storage.v2.ObjectContexts; +import com.google.storage.v2.ObjectCustomContextPayload; import com.google.storage.v2.Owner; import com.google.type.Date; import com.google.type.Expr; @@ -132,6 +135,12 @@ final class GrpcConversions { bs -> Base64.getEncoder().encodeToString(bs.toByteArray()), s -> ByteString.copyFrom(Base64.getDecoder().decode(s.getBytes(StandardCharsets.UTF_8)))); + private final Codec objectContextsCodec = + Codec.of(this::objectContextsEncode, this::objectContextsDecode); + private final Codec + customContextPayloadCodec = + Codec.of(this::objectCustomContextPayloadEncode, this::objectCustomContextPayloadDecode); + @VisibleForTesting final Codec timestampCodec = Codec.of( @@ -1007,6 +1016,7 @@ private Object blobInfoEncode(BlobInfo from) { } ifNonNull(from.getMetadata(), this::removeNullValues, toBuilder::putAllMetadata); ifNonNull(from.getAcl(), toImmutableListOf(objectAcl()::encode), toBuilder::addAllAcl); + ifNonNull(from.getContexts(), objectContextsCodec::encode, toBuilder::setContexts); return toBuilder.build(); } @@ -1086,6 +1096,9 @@ private BlobInfo blobInfoDecode(Object from) { toBuilder.setEtag(from.getEtag()); } ifNonNull(from.getAclList(), toImmutableListOf(objectAcl()::decode), toBuilder::setAcl); + if (from.hasContexts()) { + toBuilder.setContexts(objectContextsCodec.decode(from.getContexts())); + } return toBuilder.build(); } @@ -1248,6 +1261,50 @@ private IpFilter.VpcNetworkSource vpcNetworkSourceDecode(VpcNetworkSource from) return to.build(); } + private ObjectContexts objectContextsEncode(BlobInfo.ObjectContexts from) { + if (from == null) { + return null; + } + ObjectContexts.Builder to = ObjectContexts.newBuilder(); + if (from.getCustom() != null) { + to.putAllCustom( + Maps.transformValues( + Maps.filterValues(from.getCustom(), Objects::nonNull), + customContextPayloadCodec::encode)); + } + return to.build(); + } + + private BlobInfo.ObjectContexts objectContextsDecode(ObjectContexts from) { + return BlobInfo.ObjectContexts.newBuilder() + .setCustom(Maps.transformValues(from.getCustomMap(), customContextPayloadCodec::decode)) + .build(); + } + + private ObjectCustomContextPayload objectCustomContextPayloadEncode( + BlobInfo.ObjectCustomContextPayload from) { + ObjectCustomContextPayload.Builder to = ObjectCustomContextPayload.newBuilder(); + ifNonNull(from.getValue(), to::setValue); + ifNonNull(from.getCreateTime(), timestampCodec::encode, to::setCreateTime); + ifNonNull(from.getUpdateTime(), timestampCodec::encode, to::setUpdateTime); + return to.build(); + } + + private BlobInfo.ObjectCustomContextPayload objectCustomContextPayloadDecode( + ObjectCustomContextPayload from) { + BlobInfo.ObjectCustomContextPayload.Builder to = + BlobInfo.ObjectCustomContextPayload.newBuilder(); + to.setValue(from.getValue()); + + if (from.hasCreateTime()) { + to.setCreateTime(timestampCodec.decode(from.getCreateTime())); + } + if (from.hasUpdateTime()) { + to.setUpdateTime(timestampCodec.decode(from.getUpdateTime())); + } + return to.build(); + } + /** * Several properties are translating lists of one type to another. This convenience method allows * specifying a mapping function and composing as part of an {@code #isNonNull} definition. 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 016e793a5d..2938a2ede1 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 @@ -42,8 +42,10 @@ import com.google.api.services.storage.model.Bucket.Website; import com.google.api.services.storage.model.BucketAccessControl; import com.google.api.services.storage.model.ObjectAccessControl; +import com.google.api.services.storage.model.ObjectCustomContextPayload; import com.google.api.services.storage.model.Policy.Bindings; import com.google.api.services.storage.model.StorageObject; +import com.google.api.services.storage.model.StorageObject.Contexts; import com.google.api.services.storage.model.StorageObject.Owner; import com.google.cloud.Binding; import com.google.cloud.Policy; @@ -86,6 +88,7 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -252,6 +255,12 @@ final class JsonConversions { } return CustomerSuppliedEncryptionEnforcementConfig.of(mode); }); + private final Codec objectContextsCodec = + Codec.of(this::objectContextsEncode, this::objectContextsDecode); + + private final Codec + objectCustomContextPayloadCodec = + Codec.of(this::objectCustomContextPayloadEncode, this::objectCustomContextPayloadDecode); private JsonConversions() {} @@ -391,6 +400,7 @@ private StorageObject blobInfoEncode(BlobInfo from) { to.setEtag(from.getEtag()); to.setId(from.getGeneratedId()); to.setSelfLink(from.getSelfLink()); + ifNonNull(from.getContexts(), objectContextsCodec::encode, to::setContexts); return to; } @@ -437,6 +447,7 @@ private BlobInfo blobInfoDecode(StorageObject from) { ifNonNull(from.getRetention(), this::retentionDecode, to::setRetention); ifNonNull(from.getSoftDeleteTime(), dateTimeCodec::decode, to::setSoftDeleteTime); ifNonNull(from.getHardDeleteTime(), dateTimeCodec::decode, to::setHardDeleteTime); + ifNonNull(from.getContexts(), objectContextsCodec::decode, to::setContexts); return to.build(); } @@ -1242,6 +1253,49 @@ private static void maybeDecodeRetentionPolicy(Bucket from, BucketInfo.Builder t } } + private Contexts objectContextsEncode(BlobInfo.ObjectContexts from) { + if (from == null) { + return null; + } + Contexts to = new Contexts(); + ifNonNull( + from.getCustom(), + m -> new HashMap<>(Maps.transformValues(m, objectCustomContextPayloadCodec::encode)), + to::setCustom); + return to; + } + + private BlobInfo.ObjectContexts objectContextsDecode(Contexts from) { + if (from == null) { + return null; + } + BlobInfo.ObjectContexts.Builder to = BlobInfo.ObjectContexts.newBuilder(); + ifNonNull( + from.getCustom(), + m -> new HashMap<>(Maps.transformValues(m, objectCustomContextPayloadCodec::decode)), + to::setCustom); + return to.build(); + } + + private ObjectCustomContextPayload objectCustomContextPayloadEncode( + BlobInfo.ObjectCustomContextPayload from) { + ObjectCustomContextPayload to = new ObjectCustomContextPayload(); + ifNonNull(from.getValue(), to::setValue); + ifNonNull(from.getCreateTime(), Utils.dateTimeCodec::encode, to::setCreateTime); + ifNonNull(from.getUpdateTime(), Utils.dateTimeCodec::encode, to::setUpdateTime); + return to; + } + + private BlobInfo.ObjectCustomContextPayload objectCustomContextPayloadDecode( + ObjectCustomContextPayload from) { + BlobInfo.ObjectCustomContextPayload.Builder to = + BlobInfo.ObjectCustomContextPayload.newBuilder(); + ifNonNull(from.getValue(), to::setValue); + ifNonNull(from.getCreateTime(), Utils.dateTimeCodec::decode, to::setCreateTime); + ifNonNull(from.getUpdateTime(), Utils.dateTimeCodec::decode, to::setUpdateTime); + return to.build(); + } + private static Map replaceDataNullValuesWithNull(Map labels) { boolean anyDataNull = labels.values().stream().anyMatch(Data::isNull); if (anyDataNull) { 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 61597307fd..0cbe7e5c14 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 @@ -339,7 +339,11 @@ enum BlobField implements FieldSelector, NamedField { @TransportCompatibility({Transport.HTTP, Transport.GRPC}) HARD_DELETE_TIME( - "hardDeleteTime", "hard_delete_time", com.google.api.client.util.DateTime.class); + "hardDeleteTime", "hard_delete_time", com.google.api.client.util.DateTime.class), + + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + OBJECT_CONTEXTS( + "contexts", "contexts", com.google.api.services.storage.model.StorageObject.Contexts.class); static final List REQUIRED_FIELDS = ImmutableList.of(BUCKET, NAME); private static final Map JSON_FIELD_NAME_INDEX; @@ -2743,6 +2747,62 @@ public static BlobListOption softDeleted(boolean softDeleted) { return new BlobListOption(UnifiedOpts.softDeleted(softDeleted)); } + /** + * Returns an option to filter list results to objects that have a context with the specified + * key and value. + * + * @param key The context key to match. + * @param value The context value to match. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobListOption withContext(@NonNull String key, @NonNull String value) { + requireNonNull(key, "Key must be non null"); + requireNonNull(value, "Value must be non null"); + String filter = String.format("contexts.\"%s\"=\"%s\"", key, value); + return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); + } + + /** + * Returns an option to filter list results to objects that DO NOT have a context with the + * specified key and value. + * + * @param key The context key to check. + * @param value The context value to check. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobListOption withoutContext(@NonNull String key, @NonNull String value) { + requireNonNull(key, "Key must be non null"); + requireNonNull(value, "Value must be non null"); + String filter = String.format("NOT contexts.\"%s\"=\"%s\"", key, value); + return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); + } + + /** + * Returns an option to filter list results to objects that have a context with the specified + * key, regardless of its value. + * + * @param key The context key to check for presence. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobListOption withContextKey(@NonNull String key) { + requireNonNull(key, "Key must be non null"); + String filter = String.format("contexts.\"%s\":*", key); + return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); + } + + /** + * Returns an option to filter list results to objects that DO NOT have a context with the + * specified key. + * + * @param key The context key to check for absence. + */ + @TransportCompatibility({Transport.HTTP, Transport.GRPC}) + public static BlobListOption withoutContextKey(@NonNull String key) { + requireNonNull(key, "Key must be non null"); + String filter = String.format("NOT contexts.\"%s\":*", key); + return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); + } + /** * A set of extra headers to be set for all requests performed within the scope of the operation * this option is passed to (a get, read, resumable upload etc). 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 5092f1e62d..39f1ddb5c9 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 @@ -581,6 +581,10 @@ static Md5MatchExtractor md5MatchExtractor() { return Md5MatchExtractor.INSTANCE; } + static ObjectContextsFilter objectContextsFilter(String filter) { + return new ObjectContextsFilter(filter); + } + static Headers extraHeaders(ImmutableMap extraHeaders) { requireNonNull(extraHeaders, "extraHeaders must be non null"); String blockedHeaders = @@ -2502,6 +2506,19 @@ private Object readResolve() { } } + static final class ObjectContextsFilter extends RpcOptVal implements ObjectListOpt { + private static final long serialVersionUID = -892748218491324843L; + + private ObjectContextsFilter(String val) { + super(StorageRpc.Option.OBJECT_CONTEXTS_FILTER, val); + } + + @Override + public Mapper listObjects() { + return b -> b.setFilter(val); + } + } + /** * Internal only implementation of {@link ObjectTargetOpt} which is a No-op. * 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 6ea50f3772..ebb0223f98 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 @@ -497,7 +497,8 @@ public Tuple> list(final String bucket, Map storageObjects = 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 160a2ad433..2300ec2d50 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 @@ -80,6 +80,7 @@ enum Option { INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes"), INCLUDE_TRAILING_DELIMITER("includeTrailingDelimiter"), X_UPLOAD_CONTENT_LENGTH("x-upload-content-length"), + OBJECT_CONTEXTS_FILTER("objectContextsFilter"), /** * An {@link com.google.common.collect.ImmutableMap ImmutableMap<String, String>} of values * which will be set as additional headers on the request. 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 c563c9e81a..862709a5ae 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 @@ -28,9 +28,12 @@ import com.google.cloud.storage.Acl.Project; import com.google.cloud.storage.Acl.User; import com.google.cloud.storage.BlobInfo.CustomerEncryption; +import com.google.cloud.storage.BlobInfo.ObjectContexts; +import com.google.cloud.storage.BlobInfo.ObjectCustomContextPayload; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.math.BigInteger; +import java.util.Collections; import java.util.List; import java.util.Map; import org.junit.Test; @@ -79,6 +82,12 @@ public class BlobInfoTest { private static final Boolean EVENT_BASED_HOLD = true; private static final Boolean TEMPORARY_HOLD = true; private static final Long RETENTION_EXPIRATION_TIME = 10L; + private static final ObjectCustomContextPayload payload = + ObjectCustomContextPayload.newBuilder().setValue("contextValue").build(); + private static final Map customContexts = + Collections.singletonMap("contextKey", payload); + private static final ObjectContexts OBJECT_CONTEXTS = + ObjectContexts.newBuilder().setCustom(customContexts).build(); private static final BlobInfo BLOB_INFO = BlobInfo.newBuilder("b", "n", GENERATION) @@ -110,6 +119,7 @@ public class BlobInfoTest { .setEventBasedHold(EVENT_BASED_HOLD) .setTemporaryHold(TEMPORARY_HOLD) .setRetentionExpirationTime(RETENTION_EXPIRATION_TIME) + .setContexts(OBJECT_CONTEXTS) .build(); private static final BlobInfo DIRECTORY_INFO = BlobInfo.newBuilder("b", "n/").setSize(0L).setIsDirectory(true).build(); 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 f312837b0a..d52e1b7d6c 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 @@ -36,6 +36,8 @@ import com.google.cloud.storage.Acl.User; import com.google.cloud.storage.Blob.BlobSourceOption; import com.google.cloud.storage.BlobInfo.BuilderImpl; +import com.google.cloud.storage.BlobInfo.ObjectContexts; +import com.google.cloud.storage.BlobInfo.ObjectCustomContextPayload; import com.google.cloud.storage.Storage.CopyRequest; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -44,6 +46,7 @@ import java.net.URL; import java.nio.file.Path; import java.security.Key; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -92,6 +95,12 @@ public class BlobTest { private static final Boolean EVENT_BASED_HOLD = true; private static final Boolean TEMPORARY_HOLD = true; private static final Long RETENTION_EXPIRATION_TIME = 10L; + private static final ObjectCustomContextPayload payload = + ObjectCustomContextPayload.newBuilder().setValue("contextValue").build(); + private static final Map customContexts = + Collections.singletonMap("contextKey", payload); + private static final ObjectContexts OBJECT_CONTEXTS = + ObjectContexts.newBuilder().setCustom(customContexts).build(); private static final BlobInfo FULL_BLOB_INFO = BlobInfo.newBuilder("b", "n", GENERATION) .setAcl(ACLS) @@ -122,6 +131,7 @@ public class BlobTest { .setEventBasedHold(EVENT_BASED_HOLD) .setTemporaryHold(TEMPORARY_HOLD) .setRetentionExpirationTime(RETENTION_EXPIRATION_TIME) + .setContexts(OBJECT_CONTEXTS) .build(); private static final BlobInfo BLOB_INFO = BlobInfo.newBuilder("b", "n", 12345678L).setMetageneration(42L).build(); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java index bf8c48258e..5c76995a57 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITBlobReadMaskTest.java @@ -202,6 +202,7 @@ public ImmutableList parameters() { new Args<>( BlobField.RETENTION, LazyAssertion.skip("TODO: jesse fill in buganizer bug here")), + new Args<>(BlobField.OBJECT_CONTEXTS, LazyAssertion.equal()), new Args<>(BlobField.SOFT_DELETE_TIME, LazyAssertion.equal()), new Args<>(BlobField.HARD_DELETE_TIME, LazyAssertion.equal())); List argsDefined = 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 e30fc31250..28e88582f7 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 @@ -742,7 +742,8 @@ public void storage_BlobGetOption_fields_BlobField() { "updated", "retention", "softDeleteTime", - "hardDeleteTime"); + "hardDeleteTime", + "contexts"); s.get(o.getBlobId(), BlobGetOption.fields(BlobField.values())); requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet()); } @@ -923,7 +924,8 @@ public void storage_BlobListOption_fields_BlobField() { "items/updated", "items/retention", "items/softDeleteTime", - "items/hardDeleteTime"); + "items/hardDeleteTime", + "items/contexts"); s.list(b.getName(), BlobListOption.fields(BlobField.values())); requestAuditing.assertQueryParam("fields", expected, splitOnCommaToSet()); } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java index e8a5bb64b1..4b99a19da4 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/ObjectArbitraryProvider.java @@ -79,7 +79,8 @@ public Set> provideFor(TypeUsage targetType, SubtypeProvider subtyp StorageArbitraries.objects().customMetadata(), StorageArbitraries.owner().injectNull(0.1), StorageArbitraries.objects().objectAccessControl().injectNull(0.5), - StorageArbitraries.etag()) + StorageArbitraries.etag(), + StorageArbitraries.objects().objectContexts()) .as(Tuple::of)) .as( (t1, t2, t3, t4) -> { @@ -111,6 +112,7 @@ public Set> provideFor(TypeUsage targetType, SubtypeProvider subtyp ifNonNull(t3.get7(), b::setCustomerEncryption); ifNonNull(t3.get8(), b::setCustomTime); ifNonNull(t4.get4(), b::setEtag); + ifNonNull(t4.get5(), b::setContexts); return b.build(); }); return Collections.singleton(objectArbitrary); 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 b9d5bd0a54..ffcb2fcac0 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 @@ -42,6 +42,8 @@ import com.google.storage.v2.CustomerEncryption; import com.google.storage.v2.ObjectAccessControl; import com.google.storage.v2.ObjectChecksums; +import com.google.storage.v2.ObjectContexts; +import com.google.storage.v2.ObjectCustomContextPayload; import com.google.storage.v2.Owner; import com.google.storage.v2.ProjectName; import com.google.storage.v2.ProjectTeam; @@ -709,6 +711,35 @@ public Arbitrary> customMetadata() { public ListArbitrary objectAccessControl() { return buckets().objectAccessControl(); } + + public Arbitrary objectCustomContextPayload() { + return Combinators.combine( + randomString().ofMinLength(1).ofMaxLength(128), + timestamp().injectNull(0.5), + timestamp().injectNull(0.5)) + .as( + (value, createTime, updateTime) -> { + ObjectCustomContextPayload.Builder builder = + ObjectCustomContextPayload.newBuilder().setValue(value); + if (createTime != null) { + builder.setCreateTime(createTime); + } + if (updateTime != null) { + builder.setUpdateTime(updateTime); + } + return builder.build(); + }); + } + + public Arbitrary objectContexts() { + Arbitrary key = alphaString().ofMinLength(1).ofMaxLength(32); + Arbitrary> customMap = + Arbitraries.maps(key, objectCustomContextPayload()).ofMinSize(0).ofMaxSize(5); + + return customMap + .map(c -> ObjectContexts.newBuilder().putAllCustom(c).build()) + .injectNull(0.5); + } } public static HttpHeaders httpHeaders() { From 539d7440386520e8ab3a9a2d8ab433c4138c9297 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 27 Aug 2025 11:38:28 +0530 Subject: [PATCH 2/2] chore: Refactor object filter options and remove specific helpers --- .../com/google/cloud/storage/Storage.java | 54 ++----------------- .../com/google/cloud/storage/UnifiedOpts.java | 10 ++-- .../cloud/storage/spi/v1/HttpStorageRpc.java | 2 +- .../cloud/storage/spi/v1/StorageRpc.java | 2 +- 4 files changed, 11 insertions(+), 57 deletions(-) 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 0cbe7e5c14..79e270875d 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 @@ -2748,59 +2748,13 @@ public static BlobListOption softDeleted(boolean softDeleted) { } /** - * Returns an option to filter list results to objects that have a context with the specified - * key and value. + * Returns an option to filter list results based on object attributes, such as object contexts. * - * @param key The context key to match. - * @param value The context value to match. + * @param filter The filter string. */ @TransportCompatibility({Transport.HTTP, Transport.GRPC}) - public static BlobListOption withContext(@NonNull String key, @NonNull String value) { - requireNonNull(key, "Key must be non null"); - requireNonNull(value, "Value must be non null"); - String filter = String.format("contexts.\"%s\"=\"%s\"", key, value); - return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); - } - - /** - * Returns an option to filter list results to objects that DO NOT have a context with the - * specified key and value. - * - * @param key The context key to check. - * @param value The context value to check. - */ - @TransportCompatibility({Transport.HTTP, Transport.GRPC}) - public static BlobListOption withoutContext(@NonNull String key, @NonNull String value) { - requireNonNull(key, "Key must be non null"); - requireNonNull(value, "Value must be non null"); - String filter = String.format("NOT contexts.\"%s\"=\"%s\"", key, value); - return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); - } - - /** - * Returns an option to filter list results to objects that have a context with the specified - * key, regardless of its value. - * - * @param key The context key to check for presence. - */ - @TransportCompatibility({Transport.HTTP, Transport.GRPC}) - public static BlobListOption withContextKey(@NonNull String key) { - requireNonNull(key, "Key must be non null"); - String filter = String.format("contexts.\"%s\":*", key); - return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); - } - - /** - * Returns an option to filter list results to objects that DO NOT have a context with the - * specified key. - * - * @param key The context key to check for absence. - */ - @TransportCompatibility({Transport.HTTP, Transport.GRPC}) - public static BlobListOption withoutContextKey(@NonNull String key) { - requireNonNull(key, "Key must be non null"); - String filter = String.format("NOT contexts.\"%s\":*", key); - return new BlobListOption(UnifiedOpts.objectContextsFilter(filter)); + public static BlobListOption filter(String filter) { + return new BlobListOption(UnifiedOpts.objectFilter(filter)); } /** 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 39f1ddb5c9..c9cb5de1de 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 @@ -581,8 +581,8 @@ static Md5MatchExtractor md5MatchExtractor() { return Md5MatchExtractor.INSTANCE; } - static ObjectContextsFilter objectContextsFilter(String filter) { - return new ObjectContextsFilter(filter); + static ObjectFilter objectFilter(String filter) { + return new ObjectFilter(filter); } static Headers extraHeaders(ImmutableMap extraHeaders) { @@ -2506,11 +2506,11 @@ private Object readResolve() { } } - static final class ObjectContextsFilter extends RpcOptVal implements ObjectListOpt { + static final class ObjectFilter extends RpcOptVal implements ObjectListOpt { private static final long serialVersionUID = -892748218491324843L; - private ObjectContextsFilter(String val) { - super(StorageRpc.Option.OBJECT_CONTEXTS_FILTER, val); + private ObjectFilter(String val) { + super(StorageRpc.Option.OBJECT_FILTER, val); } @Override 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 ebb0223f98..ca11f96673 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 @@ -498,7 +498,7 @@ public Tuple> list(final String bucket, Map storageObjects = 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 2300ec2d50..5127fbf54b 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 @@ -80,7 +80,7 @@ enum Option { INCLUDE_FOLDERS_AS_PREFIXES("includeFoldersAsPrefixes"), INCLUDE_TRAILING_DELIMITER("includeTrailingDelimiter"), X_UPLOAD_CONTENT_LENGTH("x-upload-content-length"), - OBJECT_CONTEXTS_FILTER("objectContextsFilter"), + OBJECT_FILTER("objectFilter"), /** * An {@link com.google.common.collect.ImmutableMap ImmutableMap<String, String>} of values * which will be set as additional headers on the request.