From cbad1ca6e0ac3c7eade1c59f8364c55d41f9878d Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Tue, 14 Oct 2025 10:39:21 +0000 Subject: [PATCH 01/34] chore: Refactor retrier creation from HttpStorageOptions to StorageOptions --- .../com/google/cloud/storage/HttpStorageOptions.java | 11 +---------- .../java/com/google/cloud/storage/StorageOptions.java | 11 +++++++++++ .../java/com/example/storage/ITBucketSnippets.java | 1 + .../java/com/example/storage/ITObjectSnippets.java | 2 ++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java index b1400bcb62..a73b5b140a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java @@ -32,8 +32,6 @@ import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.spi.ServiceRpcFactory; import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory; -import com.google.cloud.storage.Retrying.DefaultRetrier; -import com.google.cloud.storage.Retrying.HttpRetrier; import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.TransportCompatibility.Transport; @@ -408,14 +406,7 @@ public Storage create(StorageOptions options) { } WriterFactory factory = blobWriteSessionConfig.createFactory(clock); StorageImpl storage = - new StorageImpl( - httpStorageOptions, - factory, - new HttpRetrier( - new DefaultRetrier( - OtelStorageDecorator.retryContextDecorator(otel), - RetryingDependencies.simple( - options.getClock(), options.getRetrySettings())))); + new StorageImpl(httpStorageOptions, factory, options.createRetrier()); return OtelStorageDecorator.decorate(storage, otel, Transport.HTTP); } catch (IOException e) { throw new IllegalStateException( diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java index 4dac2b43ef..27306b6d6f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java @@ -26,6 +26,10 @@ import com.google.cloud.storage.HttpStorageOptions.HttpStorageDefaults; import com.google.cloud.storage.HttpStorageOptions.HttpStorageFactory; import com.google.cloud.storage.HttpStorageOptions.HttpStorageRpcFactory; +import com.google.cloud.storage.Retrying.DefaultRetrier; +import com.google.cloud.storage.Retrying.HttpRetrier; +import com.google.cloud.storage.Retrying.Retrier; +import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.spi.StorageRpcFactory; @@ -68,6 +72,13 @@ public abstract class StorageOptions extends ServiceOptions Date: Tue, 14 Oct 2025 10:39:21 +0000 Subject: [PATCH 02/34] chore: Refactor retrier creation from HttpStorageOptions to StorageOptions --- .../com/google/cloud/storage/HttpStorageOptions.java | 11 +---------- .../java/com/google/cloud/storage/StorageOptions.java | 11 +++++++++++ .../java/com/example/storage/ITBucketSnippets.java | 2 ++ .../java/com/example/storage/ITObjectSnippets.java | 3 +++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java index b1400bcb62..a73b5b140a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java @@ -32,8 +32,6 @@ import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.spi.ServiceRpcFactory; import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory; -import com.google.cloud.storage.Retrying.DefaultRetrier; -import com.google.cloud.storage.Retrying.HttpRetrier; import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.TransportCompatibility.Transport; @@ -408,14 +406,7 @@ public Storage create(StorageOptions options) { } WriterFactory factory = blobWriteSessionConfig.createFactory(clock); StorageImpl storage = - new StorageImpl( - httpStorageOptions, - factory, - new HttpRetrier( - new DefaultRetrier( - OtelStorageDecorator.retryContextDecorator(otel), - RetryingDependencies.simple( - options.getClock(), options.getRetrySettings())))); + new StorageImpl(httpStorageOptions, factory, options.createRetrier()); return OtelStorageDecorator.decorate(storage, otel, Transport.HTTP); } catch (IOException e) { throw new IllegalStateException( diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java index 4dac2b43ef..27306b6d6f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java @@ -26,6 +26,10 @@ import com.google.cloud.storage.HttpStorageOptions.HttpStorageDefaults; import com.google.cloud.storage.HttpStorageOptions.HttpStorageFactory; import com.google.cloud.storage.HttpStorageOptions.HttpStorageRpcFactory; +import com.google.cloud.storage.Retrying.DefaultRetrier; +import com.google.cloud.storage.Retrying.HttpRetrier; +import com.google.cloud.storage.Retrying.Retrier; +import com.google.cloud.storage.Retrying.RetryingDependencies; import com.google.cloud.storage.Storage.BlobWriteOption; import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.spi.StorageRpcFactory; @@ -68,6 +72,13 @@ public abstract class StorageOptions extends ServiceOptions Date: Tue, 21 Oct 2025 07:01:59 +0000 Subject: [PATCH 03/34] feat: Added API for CreateMultipartUpload. --- google-cloud-storage/pom.xml | 4 + .../cloud/storage/MultipartUploadClient.java | 43 +++ .../storage/MultipartUploadClientImpl.java | 150 ++++++++++ .../MultipartUploadHttpRequestManager.java | 51 ++++ .../storage/MultipartUploadSettings.java | 32 ++ .../google/cloud/storage/ObjectLockMode.java | 76 +++++ .../model/CreateMultipartUploadRequest.java | 280 ++++++++++++++++++ .../model/CreateMultipartUploadResponse.java | 163 ++++++++++ 8 files changed, 799 insertions(+) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 1b8cb3e8f1..855a1f999a 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -19,6 +19,10 @@ 1.123.5 + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + com.google.guava guava diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java new file mode 100644 index 0000000000..2675863a96 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.storage; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalExtensionOnly; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import java.io.IOException; +import java.net.URI; + +@BetaApi +@InternalExtensionOnly +public abstract class MultipartUploadClient { + + MultipartUploadClient() {} + + public abstract CreateMultipartUploadResponse createMultipartUpload( + CreateMultipartUploadRequest request) throws IOException; + + public static MultipartUploadClient create(MultipartUploadSettings config) { + HttpStorageOptions options = config.getOptions(); + return new MultipartUploadClientImpl( + URI.create(options.getHost()), + options.getStorageRpcV1().getStorage().getRequestFactory(), + options.createRetrier(), + options); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java new file mode 100644 index 0000000000..ece7d55025 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -0,0 +1,150 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.storage; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.cloud.storage.Conversions.Decoder; +import com.google.cloud.storage.Retrying.Retrier; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.common.net.MediaType; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class MultipartUploadClientImpl extends MultipartUploadClient { + + private static final String GCS_ENDPOINT = "https://storage.googleapis.com"; + + private final MultipartUploadHttpRequestManager httpRequestManager; + private final XmlMapper xmlMapper; + private final HttpStorageOptions options; + private final Retrier retrier; + + public MultipartUploadClientImpl( + URI uri, HttpRequestFactory requestFactory, Retrier retrier, HttpStorageOptions options) { + this.httpRequestManager = new MultipartUploadHttpRequestManager(requestFactory); + this.xmlMapper = new XmlMapper(); + this.options = options; + this.retrier = retrier; + } + + private Map getGenericExtensionHeader() { + Map extensionHeaders = new HashMap<>(); + if (options.getClientLibToken() != null) { + extensionHeaders.put("x-goog-api-client", options.getClientLibToken()); + } + if (options.getProjectId() != null) { + extensionHeaders.put("x-goog-user-project", options.getProjectId()); + } + extensionHeaders.put("Date", getRfc1123Date()); + return extensionHeaders; + } + + public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) + throws IOException { + String encodedBucket = encode(request.bucket()); + String encodedKey = encode(request.key()); + String resourcePath = "/" + encodedBucket + "/" + encodedKey; + String uri = GCS_ENDPOINT + resourcePath + "?uploads"; + + String contentType; + if (request.getContentType() == null) { + contentType = "application/x-www-form-urlencoded"; + } else { + try { + contentType = MediaType.parse(request.getContentType()).toString(); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Invalid Content-Type header provided: " + request.getContentType(), e); + } + } + + HttpResponse response = + httpRequestManager.sendCreateMultipartUploadRequest( + uri, contentType, request, getExtensionHeadersForCreateMultipartUpload(request)); + + if (!response.isSuccessStatusCode()) { + String error = response.parseAsString(); + throw new RuntimeException( + "Failed to initiate upload: " + response.getStatusCode() + " " + error); + } + + return xmlMapper.readValue(response.getContent(), CreateMultipartUploadResponse.class); + } + + private Map getExtensionHeadersForCreateMultipartUpload( + CreateMultipartUploadRequest request) { + Map extensionHeaders = getGenericExtensionHeader(); + if (request.getCannedAcl() != null) { + extensionHeaders.put("x-goog-acl", request.getCannedAcl().toString()); + } + if (request.getMetadata() != null) { + for (Map.Entry entry : request.getMetadata().entrySet()) { + if (entry.getKey() != null || entry.getValue() != null) { + extensionHeaders.put("x-goog-meta-" + entry.getKey(), entry.getValue()); + } + } + } + if (request.getStorageClass() != null) { + extensionHeaders.put("x-goog-storage-class", request.getStorageClass().toString()); + } + if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) { + extensionHeaders.put("x-goog-encryption-kms-key-name", request.getKmsKeyName()); + } + // x-goog-object-lock-mode and x-goog-object-lock-retain-until-date should be specified together + // Refer: https://cloud.google.com/storage/docs/xml-api/post-object-multipart#request_headers + if (request.getObjectLockMode() != null && request.getObjectLockRetainUntilDate() != null) { + extensionHeaders.put("x-goog-object-lock-mode", request.getObjectLockMode().toString()); + extensionHeaders.put( + "x-goog-object-lock-retain-until-date", + toRfc3339String(request.getObjectLockRetainUntilDate())); + } + if (request.getCustomTime() != null) { + extensionHeaders.put("x-goog-custom-time", toRfc3339String(request.getCustomTime())); + } + return extensionHeaders; + } + + private String encode(String value) throws UnsupportedEncodingException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } + + private String toRfc3339String(Date date) { + TimeZone tz = TimeZone.getTimeZone("UTC"); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + df.setTimeZone(tz); + return df.format(date); + } + + public static String getRfc1123Date() { + return DateTimeFormatter.RFC_1123_DATE_TIME + .withZone(ZoneId.of("GMT")) + .format(ZonedDateTime.now()); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java new file mode 100644 index 0000000000..44ed1b9816 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.storage; + +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import java.io.IOException; +import java.util.Map; + +public class MultipartUploadHttpRequestManager { + + private final HttpRequestFactory requestFactory; + + public MultipartUploadHttpRequestManager(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + } + + public HttpResponse sendCreateMultipartUploadRequest( + String uri, + String contentType, + CreateMultipartUploadRequest request, + Map extensionHeaders) + throws IOException { + HttpRequest httpRequest = + requestFactory.buildPostRequest( + new GenericUrl(uri), new ByteArrayContent(contentType, new byte[0])); + httpRequest.getHeaders().setContentType(contentType); + for (Map.Entry entry : extensionHeaders.entrySet()) { + httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); + } + httpRequest.setThrowExceptionOnExecuteError(false); + return httpRequest.execute(); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java new file mode 100644 index 0000000000..87596b3ebe --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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; + +public final class MultipartUploadSettings { + private final HttpStorageOptions options; + + private MultipartUploadSettings(HttpStorageOptions options) { + this.options = options; + } + + public HttpStorageOptions getOptions() { + return options; + } + + public static MultipartUploadSettings of(HttpStorageOptions options) { + return new MultipartUploadSettings(options); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java new file mode 100644 index 0000000000..c47582b97f --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.storage; + +import com.google.api.core.ApiFunction; +import com.google.cloud.StringEnumType; +import com.google.cloud.StringEnumValue; + +/** + * Represents the object lock mode. See https://cloud.google.com/storage/docs/object-lock + * for details. + */ +public final class ObjectLockMode extends StringEnumValue { + private static final long serialVersionUID = -1882734434792102329L; + + private ObjectLockMode(String constant) { + super(constant); + } + + private static final ApiFunction CONSTRUCTOR = + new ApiFunction() { + @Override + public ObjectLockMode apply(String constant) { + return new ObjectLockMode(constant); + } + }; + + private static final StringEnumType type = + new StringEnumType(ObjectLockMode.class, CONSTRUCTOR); + + /** + * Governance mode. See https://cloud.google.com/storage/docs/object-lock + * for details. + */ + public static final ObjectLockMode GOVERNANCE = type.createAndRegister("GOVERNANCE"); + + /** + * Compliance mode. See https://cloud.google.com/storage/docs/object-lock + * for details. + */ + public static final ObjectLockMode COMPLIANCE = type.createAndRegister("COMPLIANCE"); + + /** + * Get the ObjectLockMode for the given String constant, and throw an exception if the constant is + * not recognized. + */ + public static ObjectLockMode valueOfStrict(String constant) { + return type.valueOfStrict(constant); + } + + /** Get the ObjectLockMode for the given String constant, and allow unrecognized values. */ + public static ObjectLockMode valueOf(String constant) { + return type.valueOf(constant); + } + + /** Return the known values for ObjectLockMode. */ + public static ObjectLockMode[] values() { + return type.values(); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java new file mode 100644 index 0000000000..9ec1a89ccc --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.google.cloud.storage.ObjectLockMode; +import com.google.cloud.storage.Storage.PredefinedAcl; +import com.google.cloud.storage.StorageClass; +import com.google.common.base.MoreObjects; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +public class CreateMultipartUploadRequest { + private final String bucket; + private final String key; + private final PredefinedAcl cannedAcl; + private final String contentType; + private final Map metadata; + private final StorageClass storageClass; + private final Date customTime; + private final String kmsKeyName; + private final ObjectLockMode objectLockMode; + private final Date objectLockRetainUntilDate; + + private CreateMultipartUploadRequest(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.cannedAcl = builder.cannedAcl; + this.contentType = builder.contentType; + this.metadata = builder.metadata; + this.storageClass = builder.storageClass; + this.customTime = builder.customTime; + this.kmsKeyName = builder.kmsKeyName; + this.objectLockMode = builder.objectLockMode; + this.objectLockRetainUntilDate = builder.objectLockRetainUntilDate; + } + + public String bucket() { + return bucket; + } + + public String key() { + return key; + } + + public PredefinedAcl getCannedAcl() { + return cannedAcl; + } + + public String getContentType() { + return contentType; + } + + public Map getMetadata() { + return metadata; + } + + public StorageClass getStorageClass() { + return storageClass; + } + + public Date getCustomTime() { + return customTime; + } + + public String getKmsKeyName() { + return kmsKeyName; + } + + public ObjectLockMode getObjectLockMode() { + return objectLockMode; + } + + public Date getObjectLockRetainUntilDate() { + return objectLockRetainUntilDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CreateMultipartUploadRequest)) { + return false; + } + CreateMultipartUploadRequest that = (CreateMultipartUploadRequest) o; + return Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && cannedAcl == that.cannedAcl + && Objects.equals(contentType, that.contentType) + && Objects.equals(metadata, that.metadata) + && Objects.equals(storageClass, that.storageClass) + && Objects.equals(customTime, that.customTime) + && Objects.equals(kmsKeyName, that.kmsKeyName) + && objectLockMode == that.objectLockMode + && Objects.equals(objectLockRetainUntilDate, that.objectLockRetainUntilDate); + } + + @Override + public int hashCode() { + return Objects.hash( + bucket, + key, + cannedAcl, + contentType, + metadata, + storageClass, + customTime, + kmsKeyName, + objectLockMode, + objectLockRetainUntilDate); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket) + .add("key", key) + .add("cannedAcl", cannedAcl) + .add("contentType", contentType) + .add("metadata", metadata) + .add("storageClass", storageClass) + .add("customTime", customTime) + .add("kmsKeyName", kmsKeyName) + .add("objectLockMode", objectLockMode) + .add("objectLockRetainUntilDate", objectLockRetainUntilDate) + .toString(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String bucket; + private String key; + private PredefinedAcl cannedAcl; + private String contentType; + private Map metadata; + private StorageClass storageClass; + private Date customTime; + private String kmsKeyName; + private ObjectLockMode objectLockMode; + private Date objectLockRetainUntilDate; + + private Builder() {} + + /** + * The bucket to which the object is being uploaded. + * + * @param bucket The bucket name + * @return this builder + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * The name of the object. + * + * @param key The object name + * @return this builder + */ + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * A canned ACL to apply to the object. + * + * @param cannedAcl The canned ACL + * @return this builder + */ + public Builder cannedAcl(PredefinedAcl cannedAcl) { + this.cannedAcl = cannedAcl; + return this; + } + + /** + * The MIME type of the data you are uploading. + * + * @param contentType The Content-Type + * @return this builder + */ + public Builder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + /** + * The custom metadata of the object. + * + * @param metadata The custom metadata + * @return this builder + */ + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + /** + * Gives each part of the upload and the resulting object a storage class besides the default + * storage class of the associated bucket. + * + * @param storageClass The Storage-Class + * @return this builder + */ + public Builder storageClass(StorageClass storageClass) { + this.storageClass = storageClass; + return this; + } + + /** + * A user-specified date and time. + * + * @param customTime The custom time + * @return this builder + */ + public Builder customTime(Date customTime) { + this.customTime = customTime; + return this; + } + + /** + * The customer-managed encryption key to use to encrypt the object. Refer: Customer + * Managed Keys + * + * @param kmsKeyName The Cloud KMS key + * @return this builder + */ + public Builder kmsKeyName(String kmsKeyName) { + this.kmsKeyName = kmsKeyName; + return this; + } + + /** + * Mode of the object's retention configuration. GOVERNANCE corresponds to unlocked mode, and + * COMPLIANCE corresponds to locked mode. + * + * @param objectLockMode The object lock mode + * @return this builder + */ + public Builder objectLockMode(ObjectLockMode objectLockMode) { + this.objectLockMode = objectLockMode; + return this; + } + + /** + * Date that determines the time until which the object is retained as immutable. + * + * @param objectLockRetainUntilDate The object lock retention until date + * @return this builder + */ + public Builder objectLockRetainUntilDate(Date objectLockRetainUntilDate) { + this.objectLockRetainUntilDate = objectLockRetainUntilDate; + return this; + } + + public CreateMultipartUploadRequest build() { + return new CreateMultipartUploadRequest(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java new file mode 100644 index 0000000000..cd32bb1b99 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -0,0 +1,163 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** + * Represents the response from a CreateMultipartUpload request. This class encapsulates the details + * of the initiated multipart upload, including the bucket, key, and the unique upload ID. + */ +@JacksonXmlRootElement(localName = "InitiateMultipartUploadResult") +public class CreateMultipartUploadResponse { + + @JacksonXmlProperty(localName = "Bucket") + private String bucket; + + @JacksonXmlProperty(localName = "Key") + private String key; + + @JacksonXmlProperty(localName = "UploadId") + private String uploadId; + + private CreateMultipartUploadResponse(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.uploadId = builder.uploadId; + } + + private CreateMultipartUploadResponse() {} + + /** + * Returns the name of the bucket where the multipart upload was initiated. + * + * @return The bucket name. + */ + public String bucket() { + return bucket; + } + + /** + * Returns the key (object name) for which the multipart upload was initiated. + * + * @return The object key. + */ + public String key() { + return key; + } + + /** + * Returns the unique identifier for this multipart upload. This ID must be included in all + * subsequent requests related to this upload (e.g., uploading parts, completing the upload). + * + * @return The upload ID. + */ + public String uploadId() { + return uploadId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CreateMultipartUploadResponse)) { + return false; + } + CreateMultipartUploadResponse that = (CreateMultipartUploadResponse) o; + return Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && Objects.equals(uploadId, that.uploadId); + } + + @Override + public int hashCode() { + return Objects.hash(bucket, key, uploadId); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket) + .add("key", key) + .add("uploadId", uploadId) + .toString(); + } + + /** + * Creates a new builder for {@link CreateMultipartUploadResponse}. + * + * @return A new builder. + */ + public static Builder builder() { + return new Builder(); + } + + /** A builder for {@link CreateMultipartUploadResponse} objects. */ + public static class Builder { + private String bucket; + private String key; + private String uploadId; + + private Builder() {} + + /** + * Sets the bucket name for the multipart upload. + * + * @param bucket The bucket name. + * @return This builder. + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the key (object name) for the multipart upload. + * + * @param key The object key. + * @return This builder. + */ + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * Sets the upload ID for the multipart upload. + * + * @param uploadId The upload ID. + * @return This builder. + */ + public Builder uploadId(String uploadId) { + this.uploadId = uploadId; + return this; + } + + /** + * Builds a new {@link CreateMultipartUploadResponse} object. + * + * @return A new {@link CreateMultipartUploadResponse} object. + */ + public CreateMultipartUploadResponse build() { + return new CreateMultipartUploadResponse(this); + } + } +} From 3bfd51b93dbb8feed654e97598088b0da835c108 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Tue, 21 Oct 2025 18:41:14 +0000 Subject: [PATCH 04/34] added missing javadoc --- .../multipartupload/model/CreateMultipartUploadRequest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index 8398182ce2..b52eeebb61 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -24,6 +24,10 @@ import java.util.Map; import java.util.Objects; +/** + * Represents a request to initiate a multipart upload. + * This class holds all the necessary information to create a new multipart upload session. + */ public final class CreateMultipartUploadRequest { private final String bucket; private final String key; From 11570046edb2b0b36ca157eb488c8ea13bbd5f42 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Tue, 21 Oct 2025 18:47:17 +0000 Subject: [PATCH 05/34] Fixed lint issues --- .../multipartupload/model/CreateMultipartUploadRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index b52eeebb61..0cd121a9b4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -25,8 +25,8 @@ import java.util.Objects; /** - * Represents a request to initiate a multipart upload. - * This class holds all the necessary information to create a new multipart upload session. + * Represents a request to initiate a multipart upload. This class holds all the necessary + * information to create a new multipart upload session. */ public final class CreateMultipartUploadRequest { private final String bucket; From 585707d1d1fde5d6a573166486f6cde8905590db Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 09:06:49 +0000 Subject: [PATCH 06/34] chore: adding review changes. --- .../cloud/storage/MultipartUploadClient.java | 2 + .../storage/MultipartUploadClientImpl.java | 14 ++-- .../storage/MultipartUploadSettings.java | 5 ++ .../google/cloud/storage/ObjectLockMode.java | 2 + .../model/CreateMultipartUploadRequest.java | 82 ++++++++++++++++--- .../model/CreateMultipartUploadResponse.java | 4 +- 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 6a0d28112c..0d1a560034 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -37,9 +37,11 @@ public abstract class MultipartUploadClient { MultipartUploadClient() {} + @BetaApi public abstract CreateMultipartUploadResponse createMultipartUpload( CreateMultipartUploadRequest request) throws IOException; + @BetaApi public static MultipartUploadClient create(MultipartUploadSettings config) { HttpStorageOptions options = config.getOptions(); return new MultipartUploadClientImpl( diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index 6d070c9e54..fa3ac2b744 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -16,6 +16,7 @@ package com.google.cloud.storage; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.google.api.core.BetaApi; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpResponse; import com.google.cloud.storage.Retrying.Retrier; @@ -27,19 +28,18 @@ import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.TimeZone; /** * This class is an implementation of {@link MultipartUploadClient} that uses the Google Cloud * Storage XML API to perform multipart uploads. */ +@BetaApi public final class MultipartUploadClientImpl extends MultipartUploadClient { private final MultipartUploadHttpRequestManager httpRequestManager; @@ -69,6 +69,7 @@ private Map getGenericExtensionHeader() { return extensionHeaders; } + @BetaApi public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) throws IOException { String encodedBucket = encode(request.bucket()); @@ -138,11 +139,8 @@ private String encode(String value) throws UnsupportedEncodingException { return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); } - private String toRfc3339String(Date date) { - TimeZone tz = TimeZone.getTimeZone("UTC"); - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - df.setTimeZone(tz); - return df.format(date); + private String toRfc3339String(OffsetDateTime dateTime) { + return DateTimeFormatter.ISO_INSTANT.format(dateTime); } public static String getRfc1123Date() { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java index 596020e8aa..fbf55b3bfd 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadSettings.java @@ -15,11 +15,14 @@ */ package com.google.cloud.storage; +import com.google.api.core.BetaApi; + /** * Settings for configuring the {@link MultipartUploadClient}. * *

This class is for internal use only and is not intended for public consumption. */ +@BetaApi public final class MultipartUploadSettings { private final HttpStorageOptions options; @@ -37,6 +40,7 @@ private MultipartUploadSettings(HttpStorageOptions options) { * * @return The {@link HttpStorageOptions}. */ + @BetaApi public HttpStorageOptions getOptions() { return options; } @@ -48,6 +52,7 @@ public HttpStorageOptions getOptions() { * @param options The {@link HttpStorageOptions} to use. * @return A new {@code MultipartUploadSettings} instance. */ + @BetaApi public static MultipartUploadSettings of(HttpStorageOptions options) { return new MultipartUploadSettings(options); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java index c47582b97f..798df8db9b 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java @@ -18,12 +18,14 @@ import com.google.api.core.ApiFunction; import com.google.cloud.StringEnumType; import com.google.cloud.StringEnumValue; +import com.google.api.core.BetaApi; /** * Represents the object lock mode. See https://cloud.google.com/storage/docs/object-lock * for details. */ +@BetaApi public final class ObjectLockMode extends StringEnumValue { private static final long serialVersionUID = -1882734434792102329L; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index 0cd121a9b4..ce33c90249 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -16,11 +16,12 @@ package com.google.cloud.storage.multipartupload.model; +import com.google.api.core.BetaApi; import com.google.cloud.storage.ObjectLockMode; import com.google.cloud.storage.Storage.PredefinedAcl; import com.google.cloud.storage.StorageClass; import com.google.common.base.MoreObjects; -import java.util.Date; +import java.time.OffsetDateTime; import java.util.Map; import java.util.Objects; @@ -28,6 +29,7 @@ * Represents a request to initiate a multipart upload. This class holds all the necessary * information to create a new multipart upload session. */ +@BetaApi public final class CreateMultipartUploadRequest { private final String bucket; private final String key; @@ -35,10 +37,10 @@ public final class CreateMultipartUploadRequest { private final String contentType; private final Map metadata; private final StorageClass storageClass; - private final Date customTime; + private final OffsetDateTime customTime; private final String kmsKeyName; private final ObjectLockMode objectLockMode; - private final Date objectLockRetainUntilDate; + private final OffsetDateTime objectLockRetainUntilDate; private CreateMultipartUploadRequest(Builder builder) { this.bucket = builder.bucket; @@ -53,43 +55,93 @@ private CreateMultipartUploadRequest(Builder builder) { this.objectLockRetainUntilDate = builder.objectLockRetainUntilDate; } + /** + * Returns the name of the bucket to which the object is being uploaded. + * + * @return The bucket name + */ public String bucket() { return bucket; } + /** + * Returns the name of the object. + * + * @return The object name + */ public String key() { return key; } + /** + * Returns a canned ACL to apply to the object. + * + * @return The canned ACL + */ public PredefinedAcl getCannedAcl() { return cannedAcl; } + /** + * Returns the MIME type of the data you are uploading. + * + * @return The Content-Type + */ public String getContentType() { return contentType; } + /** + * Returns the custom metadata of the object. + * + * @return The custom metadata + */ public Map getMetadata() { return metadata; } + /** + * Returns the storage class for the object. + * + * @return The Storage-Class + */ public StorageClass getStorageClass() { return storageClass; } - public Date getCustomTime() { + /** + * Returns a user-specified date and time. + * + * @return The custom time + */ + public OffsetDateTime getCustomTime() { return customTime; } + /** + * Returns the customer-managed encryption key to use to encrypt the object. + * + * @return The Cloud KMS key + */ public String getKmsKeyName() { return kmsKeyName; } + /** + * Returns the mode of the object's retention configuration. + * + * @return The object lock mode + */ public ObjectLockMode getObjectLockMode() { return objectLockMode; } - public Date getObjectLockRetainUntilDate() { + /** + * Returns the date that determines the time until which the object is retained as immutable. + * + * @return The object lock retention until date + */ + public OffsetDateTime getObjectLockRetainUntilDate() { return objectLockRetainUntilDate; } @@ -145,21 +197,26 @@ public String toString() { .toString(); } + /** + * Returns a new {@link Builder} for creating a {@link CreateMultipartUploadRequest}. + * + * @return a new builder + */ public static Builder builder() { return new Builder(); } - public static class Builder { + public static final class Builder { private String bucket; private String key; private PredefinedAcl cannedAcl; private String contentType; private Map metadata; private StorageClass storageClass; - private Date customTime; + private OffsetDateTime customTime; private String kmsKeyName; private ObjectLockMode objectLockMode; - private Date objectLockRetainUntilDate; + private OffsetDateTime objectLockRetainUntilDate; private Builder() {} @@ -236,7 +293,7 @@ public Builder storageClass(StorageClass storageClass) { * @param customTime The custom time * @return this builder */ - public Builder customTime(Date customTime) { + public Builder customTime(OffsetDateTime customTime) { this.customTime = customTime; return this; } @@ -272,11 +329,16 @@ public Builder objectLockMode(ObjectLockMode objectLockMode) { * @param objectLockRetainUntilDate The object lock retention until date * @return this builder */ - public Builder objectLockRetainUntilDate(Date objectLockRetainUntilDate) { + public Builder objectLockRetainUntilDate(OffsetDateTime objectLockRetainUntilDate) { this.objectLockRetainUntilDate = objectLockRetainUntilDate; return this; } + /** + * Creates a new {@link CreateMultipartUploadRequest} object. + * + * @return a new {@link CreateMultipartUploadRequest} object + */ public CreateMultipartUploadRequest build() { return new CreateMultipartUploadRequest(this); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java index a29a7ee2b9..46f86791a8 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -16,6 +16,7 @@ package com.google.cloud.storage.multipartupload.model; +import com.google.api.core.BetaApi; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.google.common.base.MoreObjects; @@ -26,6 +27,7 @@ * of the initiated multipart upload, including the bucket, key, and the unique upload ID. */ @JacksonXmlRootElement(localName = "InitiateMultipartUploadResult") +@BetaApi public final class CreateMultipartUploadResponse { @JacksonXmlProperty(localName = "Bucket") @@ -111,7 +113,7 @@ public static Builder builder() { } /** A builder for {@link CreateMultipartUploadResponse} objects. */ - public static class Builder { + public static final class Builder { private String bucket; private String key; private String uploadId; From eca5d549c6c61268f991c297c2e1fbc2ca35c351 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 14:55:59 +0000 Subject: [PATCH 07/34] chore: addind review comment fixes --- .../storage/MultipartUploadClientImpl.java | 110 +--------- .../MultipartUploadHttpRequestManager.java | 93 +++++++-- .../google/cloud/storage/ObjectLockMode.java | 2 +- .../google/cloud/storage/XmlObjectParser.java | 53 +++++ .../model/CreateMultipartUploadResponse.java | 2 +- .../MultipartUploadClientImplTest.java | 192 ------------------ ...MultipartUploadHttpRequestManagerTest.java | 69 ------- 7 files changed, 142 insertions(+), 379 deletions(-) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java delete mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadClientImplTest.java delete mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadHttpRequestManagerTest.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index fa3ac2b744..8c333455a7 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -16,136 +16,38 @@ package com.google.cloud.storage; import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.google.api.core.BetaApi; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; +import com.google.api.core.BetaApi; import com.google.cloud.storage.Retrying.Retrier; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; -import com.google.common.net.MediaType; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; /** * This class is an implementation of {@link MultipartUploadClient} that uses the Google Cloud * Storage XML API to perform multipart uploads. */ @BetaApi -public final class MultipartUploadClientImpl extends MultipartUploadClient { +final class MultipartUploadClientImpl extends MultipartUploadClient { private final MultipartUploadHttpRequestManager httpRequestManager; - private final XmlMapper xmlMapper; private final HttpStorageOptions options; private final Retrier retrier; private final URI uri; - public MultipartUploadClientImpl( + MultipartUploadClientImpl( URI uri, HttpRequestFactory requestFactory, Retrier retrier, HttpStorageOptions options) { - this.httpRequestManager = new MultipartUploadHttpRequestManager(requestFactory); - this.xmlMapper = new XmlMapper(); + this.httpRequestManager = + new MultipartUploadHttpRequestManager(requestFactory, new XmlObjectParser(new XmlMapper())); this.options = options; this.retrier = retrier; this.uri = uri; } - private Map getGenericExtensionHeader() { - Map extensionHeaders = new HashMap<>(); - if (options.getClientLibToken() != null) { - extensionHeaders.put("x-goog-api-client", options.getClientLibToken()); - } - if (options.getProjectId() != null) { - extensionHeaders.put("x-goog-user-project", options.getProjectId()); - } - extensionHeaders.put("Date", getRfc1123Date()); - return extensionHeaders; - } - @BetaApi public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) throws IOException { - String encodedBucket = encode(request.bucket()); - String encodedKey = encode(request.key()); - String resourcePath = "/" + encodedBucket + "/" + encodedKey; - String createUri = uri.toString() + resourcePath + "?uploads"; - - String contentType; - if (request.getContentType() == null) { - contentType = "application/x-www-form-urlencoded"; - } else { - try { - contentType = MediaType.parse(request.getContentType()).toString(); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Invalid Content-Type header provided: " + request.getContentType(), e); - } - } - - HttpResponse response = - httpRequestManager.sendCreateMultipartUploadRequest( - createUri, contentType, request, getExtensionHeadersForCreateMultipartUpload(request)); - - if (!response.isSuccessStatusCode()) { - String error = response.parseAsString(); - throw new RuntimeException( - "Failed to initiate upload: " + response.getStatusCode() + " " + error); - } - - return xmlMapper.readValue(response.getContent(), CreateMultipartUploadResponse.class); - } - - private Map getExtensionHeadersForCreateMultipartUpload( - CreateMultipartUploadRequest request) { - Map extensionHeaders = getGenericExtensionHeader(); - if (request.getCannedAcl() != null) { - extensionHeaders.put("x-goog-acl", request.getCannedAcl().toString()); - } - if (request.getMetadata() != null) { - for (Map.Entry entry : request.getMetadata().entrySet()) { - if (entry.getKey() != null || entry.getValue() != null) { - extensionHeaders.put("x-goog-meta-" + entry.getKey(), entry.getValue()); - } - } - } - if (request.getStorageClass() != null) { - extensionHeaders.put("x-goog-storage-class", request.getStorageClass().toString()); - } - if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) { - extensionHeaders.put("x-goog-encryption-kms-key-name", request.getKmsKeyName()); - } - // x-goog-object-lock-mode and x-goog-object-lock-retain-until-date should be specified together - // Refer: https://cloud.google.com/storage/docs/xml-api/post-object-multipart#request_headers - if (request.getObjectLockMode() != null && request.getObjectLockRetainUntilDate() != null) { - extensionHeaders.put("x-goog-object-lock-mode", request.getObjectLockMode().toString()); - extensionHeaders.put( - "x-goog-object-lock-retain-until-date", - toRfc3339String(request.getObjectLockRetainUntilDate())); - } - if (request.getCustomTime() != null) { - extensionHeaders.put("x-goog-custom-time", toRfc3339String(request.getCustomTime())); - } - return extensionHeaders; - } - - private String encode(String value) throws UnsupportedEncodingException { - return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); - } - - private String toRfc3339String(OffsetDateTime dateTime) { - return DateTimeFormatter.ISO_INSTANT.format(dateTime); - } - - public static String getRfc1123Date() { - return DateTimeFormatter.RFC_1123_DATE_TIME - .withZone(ZoneId.of("GMT")) - .format(ZonedDateTime.now()); + return httpRequestManager.sendCreateMultipartUploadRequest(uri, request, options); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index 27137852c6..809861db2a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -19,33 +19,102 @@ import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; +import com.google.api.client.util.ObjectParser; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; import java.util.Map; final class MultipartUploadHttpRequestManager { private final HttpRequestFactory requestFactory; + private final ObjectParser objectParser; - MultipartUploadHttpRequestManager(HttpRequestFactory requestFactory) { + MultipartUploadHttpRequestManager(HttpRequestFactory requestFactory, ObjectParser objectParser) { this.requestFactory = requestFactory; + this.objectParser = objectParser; } - HttpResponse sendCreateMultipartUploadRequest( - String uri, - String contentType, - CreateMultipartUploadRequest request, - Map extensionHeaders) + CreateMultipartUploadResponse sendCreateMultipartUploadRequest( + URI uri, CreateMultipartUploadRequest request, HttpStorageOptions options) throws IOException { + + String encodedBucket = encode(request.bucket()); + String encodedKey = encode(request.key()); + String resourcePath = "/" + encodedBucket + "/" + encodedKey; + String createUri = uri.toString() + resourcePath + "?uploads"; + HttpRequest httpRequest = requestFactory.buildPostRequest( - new GenericUrl(uri), new ByteArrayContent(contentType, new byte[0])); - httpRequest.getHeaders().setContentType(contentType); - for (Map.Entry entry : extensionHeaders.entrySet()) { + new GenericUrl(createUri), + new ByteArrayContent(request.getContentType(), new byte[0])); + httpRequest.getHeaders().setContentType(request.getContentType()); + for (Map.Entry entry : + getExtensionHeadersForCreateMultipartUpload(request, options).entrySet()) { httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); } - httpRequest.setThrowExceptionOnExecuteError(false); - return httpRequest.execute(); + httpRequest.setParser(objectParser); + httpRequest.setThrowExceptionOnExecuteError(true); + return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class); + } + + private Map getExtensionHeadersForCreateMultipartUpload( + CreateMultipartUploadRequest request, HttpStorageOptions options) { + Map extensionHeaders = getGenericExtensionHeader(options); + if (request.getCannedAcl() != null) { + extensionHeaders.put("x-goog-acl", request.getCannedAcl().toString()); + } + //TODO(shreyassinha) Add encoding for x-goog-meta-* headers + if (request.getMetadata() != null) { + for (Map.Entry entry : request.getMetadata().entrySet()) { + if (entry.getKey() != null || entry.getValue() != null) { + extensionHeaders.put("x-goog-meta-" + entry.getKey(), entry.getValue()); + } + } + } + if (request.getStorageClass() != null) { + extensionHeaders.put("x-goog-storage-class", request.getStorageClass().toString()); + } + if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) { + extensionHeaders.put("x-goog-encryption-kms-key-name", request.getKmsKeyName()); + } + if (request.getObjectLockMode() != null) { + extensionHeaders.put("x-goog-object-lock-mode", request.getObjectLockMode().toString()); + } + if (request.getObjectLockRetainUntilDate() != null) { + extensionHeaders.put( + "x-goog-object-lock-retain-until-date", + toRfc3339String(request.getObjectLockRetainUntilDate())); + } + if (request.getCustomTime() != null) { + extensionHeaders.put("x-goog-custom-time", toRfc3339String(request.getCustomTime())); + } + return extensionHeaders; + } + + private Map getGenericExtensionHeader(HttpStorageOptions options) { + Map extensionHeaders = new HashMap<>(); + if (options.getClientLibToken() != null) { + extensionHeaders.put("x-goog-api-client", options.getClientLibToken()); + } + if (options.getProjectId() != null) { + extensionHeaders.put("x-goog-user-project", options.getProjectId()); + } + return extensionHeaders; + } + + private String encode(String value) throws UnsupportedEncodingException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + } + + private String toRfc3339String(OffsetDateTime dateTime) { + return DateTimeFormatter.ISO_INSTANT.format(dateTime); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java index 798df8db9b..f08ec2e08f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java @@ -16,9 +16,9 @@ package com.google.cloud.storage; import com.google.api.core.ApiFunction; +import com.google.api.core.BetaApi; import com.google.cloud.StringEnumType; import com.google.cloud.StringEnumValue; -import com.google.api.core.BetaApi; /** * Represents the object lock mode. See T parseAndClose(InputStream in, Charset charset, Class dataClass) + throws IOException { + return xmlMapper.readValue(in, dataClass); + } + + @Override + public Object parseAndClose(InputStream in, Charset charset, Type dataType) throws IOException { + return xmlMapper.readValue(in, xmlMapper.getTypeFactory().constructType(dataType)); + } + + @Override + public T parseAndClose(Reader reader, Class dataClass) throws IOException { + return xmlMapper.readValue(reader, dataClass); + } + + @Override + public Object parseAndClose(Reader reader, Type dataType) throws IOException { + return xmlMapper.readValue(reader, xmlMapper.getTypeFactory().constructType(dataType)); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java index 46f86791a8..35d2b2c171 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -16,9 +16,9 @@ package com.google.cloud.storage.multipartupload.model; -import com.google.api.core.BetaApi; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.google.api.core.BetaApi; import com.google.common.base.MoreObjects; import java.util.Objects; diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadClientImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadClientImplTest.java deleted file mode 100644 index 8408b93cc7..0000000000 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadClientImplTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2025 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 static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.LowLevelHttpRequest; -import com.google.api.client.http.LowLevelHttpResponse; -import com.google.api.client.json.Json; -import com.google.api.client.testing.http.MockHttpTransport; -import com.google.api.client.testing.http.MockLowLevelHttpRequest; -import com.google.api.client.testing.http.MockLowLevelHttpResponse; -import com.google.cloud.storage.Retrying.Retrier; -import com.google.cloud.storage.Storage.PredefinedAcl; -import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; -import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.Map; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@RunWith(JUnit4.class) -public class MultipartUploadClientImplTest { - - private static final String BUCKET = "bucket"; - private static final String KEY = "key"; - private static final String UPLOAD_ID = "uploadId"; - private static final String CONTENT_TYPE = "application/octet-stream"; - private static final String PROJECT_ID = "project-id"; - - private MultipartUploadClientImpl multipartUploadClient; - - @Mock private MultipartUploadHttpRequestManager httpRequestManager; - - @Mock private Retrier retrier; - - @Captor private ArgumentCaptor> extensionHeadersCaptor; - - @Captor private ArgumentCaptor uriCaptor; - - private final XmlMapper xmlMapper = new XmlMapper(); - private AutoCloseable mocks; - - @Before - public void setUp() throws Exception { - mocks = MockitoAnnotations.openMocks(this); - HttpStorageOptions options = HttpStorageOptions.newBuilder().setProjectId(PROJECT_ID).build(); - multipartUploadClient = - new MultipartUploadClientImpl( - new URI("https://storage.googleapis.com"), null, retrier, options); - // Replace the httpRequestManager with a mock - java.lang.reflect.Field field = - MultipartUploadClientImpl.class.getDeclaredField("httpRequestManager"); - field.setAccessible(true); - field.set(multipartUploadClient, httpRequestManager); - } - - @After - public void tearDown() throws Exception { - mocks.close(); - } - - @Test - public void testCreateMultipartUpload() throws IOException { - CreateMultipartUploadRequest request = - CreateMultipartUploadRequest.builder() - .bucket(BUCKET) - .key(KEY) - .contentType(CONTENT_TYPE) - .build(); - CreateMultipartUploadResponse expectedResponse = - CreateMultipartUploadResponse.builder().bucket(BUCKET).key(KEY).uploadId(UPLOAD_ID).build(); - String responseXml = xmlMapper.writeValueAsString(expectedResponse); - HttpResponse httpResponse = createHttpResponse(200, responseXml); - - when(httpRequestManager.sendCreateMultipartUploadRequest( - any(String.class), any(String.class), any(), any())) - .thenReturn(httpResponse); - - CreateMultipartUploadResponse actualResponse = - multipartUploadClient.createMultipartUpload(request); - - assertThat(actualResponse.bucket()).isEqualTo(expectedResponse.bucket()); - assertThat(actualResponse.key()).isEqualTo(expectedResponse.key()); - assertThat(actualResponse.uploadId()).isEqualTo(expectedResponse.uploadId()); - } - - @Test - public void testCreateMultipartUpload_withHeaders() throws IOException { - Map metadata = Collections.singletonMap("key", "value"); - CreateMultipartUploadRequest request = - CreateMultipartUploadRequest.builder() - .bucket(BUCKET) - .key(KEY) - .contentType(CONTENT_TYPE) - .cannedAcl(PredefinedAcl.PRIVATE) - .metadata(metadata) - .storageClass(StorageClass.COLDLINE) - .build(); - CreateMultipartUploadResponse expectedResponse = - CreateMultipartUploadResponse.builder().bucket(BUCKET).key(KEY).uploadId(UPLOAD_ID).build(); - String responseXml = xmlMapper.writeValueAsString(expectedResponse); - HttpResponse httpResponse = createHttpResponse(200, responseXml); - - when(httpRequestManager.sendCreateMultipartUploadRequest( - any(String.class), any(String.class), any(), extensionHeadersCaptor.capture())) - .thenReturn(httpResponse); - - multipartUploadClient.createMultipartUpload(request); - - Map capturedHeaders = extensionHeadersCaptor.getValue(); - assertThat(capturedHeaders).containsEntry("x-goog-acl", "PRIVATE"); - assertThat(capturedHeaders).containsEntry("x-goog-meta-key", "value"); - assertThat(capturedHeaders).containsEntry("x-goog-storage-class", "COLDLINE"); - } - - @Test - public void testCreateMultipartUpload_error() throws IOException { - CreateMultipartUploadRequest request = - CreateMultipartUploadRequest.builder() - .bucket(BUCKET) - .key(KEY) - .contentType(CONTENT_TYPE) - .build(); - HttpResponse httpResponse = createHttpResponse(500, "Internal Server Error"); - - when(httpRequestManager.sendCreateMultipartUploadRequest( - any(String.class), any(String.class), any(), any())) - .thenReturn(httpResponse); - - RuntimeException exception = - assertThrows( - RuntimeException.class, () -> multipartUploadClient.createMultipartUpload(request)); - assertThat(exception.getMessage()) - .isEqualTo("Failed to initiate upload: 500 Internal Server Error"); - } - - private HttpResponse createHttpResponse(int statusCode, String content) throws IOException { - HttpTransport transport = - new MockHttpTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) { - return new MockLowLevelHttpRequest() { - @Override - public LowLevelHttpResponse execute() { - MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); - response.setStatusCode(statusCode); - response.setContentType(Json.MEDIA_TYPE); - response.setContent(content); - return response; - } - }; - } - }; - HttpRequestFactory requestFactory = transport.createRequestFactory(); - HttpRequest request = - requestFactory.buildGetRequest(new GenericUrl("https://storage.googleapis.com")); - request.setThrowExceptionOnExecuteError(false); - return request.execute(); - } -} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadHttpRequestManagerTest.java deleted file mode 100644 index 4f12c2badb..0000000000 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/MultipartUploadHttpRequestManagerTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2024 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.api.client.http.HttpTransport; -import com.google.api.client.http.LowLevelHttpRequest; -import com.google.api.client.testing.http.MockHttpTransport; -import com.google.api.client.testing.http.MockLowLevelHttpRequest; -import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class MultipartUploadHttpRequestManagerTest { - - private static final String BUCKET = "bucket"; - private static final String KEY = "key"; - private static final String CONTENT_TYPE = "application/octet-stream"; - private static final String URI = "https://storage.googleapis.com/" + BUCKET + "/" + KEY; - - @Test - public void testSendCreateMultipartUploadRequest() throws IOException { - final AtomicReference capturedMethod = new AtomicReference<>(); - final MockLowLevelHttpRequest lowLevelRequest = new MockLowLevelHttpRequest(); - HttpTransport transport = - new MockHttpTransport() { - @Override - public LowLevelHttpRequest buildRequest(String method, String url) { - capturedMethod.set(method); - lowLevelRequest.setUrl(url); - return lowLevelRequest; - } - }; - MultipartUploadHttpRequestManager httpRequestManager = - new MultipartUploadHttpRequestManager(transport.createRequestFactory()); - - CreateMultipartUploadRequest createRequest = - CreateMultipartUploadRequest.builder().bucket(BUCKET).key(KEY).build(); - Map headers = Collections.singletonMap("x-goog-test-header", "test-value"); - - httpRequestManager.sendCreateMultipartUploadRequest(URI, CONTENT_TYPE, createRequest, headers); - - assertThat(capturedMethod.get()).isEqualTo("POST"); - assertThat(lowLevelRequest.getUrl()).isEqualTo(URI); - assertThat(lowLevelRequest.getHeaderValues("x-goog-test-header")).containsExactly("test-value"); - assertThat(lowLevelRequest.getContentAsString()).isEqualTo(""); - } -} From 04ff3d5d6d4f2fe8ca0c8f04ca06310416dbdc1f Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 16:18:32 +0000 Subject: [PATCH 08/34] chore: added test cases --- ...MultipartUploadHttpRequestManagerTest.java | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java new file mode 100644 index 0000000000..cceb908fc5 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -0,0 +1,375 @@ +/* + * Copyright 2024 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 static io.grpc.netty.shaded.io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; +import static io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus.OK; +import static org.junit.Assert.assertThrows; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; +import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; +import com.google.cloud.storage.it.runner.StorageITRunner; +import com.google.cloud.storage.it.runner.annotations.Backend; +import com.google.cloud.storage.it.runner.annotations.ParallelFriendly; +import com.google.cloud.storage.it.runner.annotations.SingleBackend; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; +import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.common.collect.ImmutableMap; +import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; +import io.grpc.netty.shaded.io.netty.buffer.Unpooled; +import io.grpc.netty.shaded.io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.grpc.netty.shaded.io.netty.handler.codec.http.FullHttpResponse; +import io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus; +import java.net.URI; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +@RunWith(StorageITRunner.class) +@SingleBackend(Backend.PROD) +@ParallelFriendly +public final class ITMultipartUploadHttpRequestManagerTest { + private static final GsonFactory gson = GsonFactory.getDefaultInstance(); + private static final NetHttpTransport transport = new NetHttpTransport.Builder().build(); + private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager; + private HttpStorageOptions httpStorageOptions; + + @Rule public final TemporaryFolder temp = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + multipartUploadHttpRequestManager = + new MultipartUploadHttpRequestManager( + transport.createRequestFactory(), new JsonObjectParser(gson)); + httpStorageOptions = HttpStorageOptions.newBuilder().setProjectId("test-project").build(); + } + + @Test + public void sendCreateMultipartUploadRequest_success() throws Exception { + HttpRequestHandler handler = + req -> { + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .build(); + + CreateMultipartUploadResponse response = + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + + assertThat(response).isNotNull(); + assertThat(response.bucket()).isEqualTo("test-bucket"); + assertThat(response.key()).isEqualTo("test-key"); + assertThat(response.uploadId()).isEqualTo("test-upload-id"); + } + } + + @Test + public void sendCreateMultipartUploadRequest_error() throws Exception { + HttpRequestHandler handler = + req -> { + FullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.BAD_REQUEST); + resp.headers().set(CONTENT_TYPE, "text/plain; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .build(); + + StorageException se = + assertThrows( + StorageException.class, + () -> + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions)); + assertThat(se.getCode()).isEqualTo(400); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withCannedAcl() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-acl")).isEqualTo("authenticatedRead"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .cannedAcl(Storage.PredefinedAcl.AUTHENTICATED_READ) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withMetadata() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-meta-key1")).isEqualTo("value1"); + assertThat(req.headers().get("x-goog-meta-key2")).isEqualTo("value2"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .metadata(ImmutableMap.of("key1", "value1", "key2", "value2")) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withStorageClass() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-storage-class")).isEqualTo("ARCHIVE"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .storageClass(StorageClass.ARCHIVE) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withKmsKeyName() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-encryption-kms-key-name")) + .isEqualTo("projects/p/locations/l/keyRings/r/cryptoKeys/k"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .kmsKeyName("projects/p/locations/l/keyRings/r/cryptoKeys/k") + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withObjectLockMode() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-object-lock-mode")).isEqualTo("GOVERNANCE"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .objectLockMode(ObjectLockMode.GOVERNANCE) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withObjectLockRetainUntilDate() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-object-lock-retain-until-date")) + .isEqualTo("2024-01-01T00:00:00Z"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .objectLockRetainUntilDate(OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } + + @Test + public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { + HttpRequestHandler handler = + req -> { + assertThat(req.headers().get("x-goog-custom-time")).isEqualTo("2024-01-01T00:00:00Z"); + CreateMultipartUploadResponse response = + CreateMultipartUploadResponse.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + CreateMultipartUploadRequest request = + CreateMultipartUploadRequest.builder() + .bucket("test-bucket") + .key("test-key") + .contentType("application/octet-stream") + .customTime(OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .build(); + + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions); + } + } +} From f4e30f9ae65e5f59d4eceb0507de843c45d8be8f Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 16:52:17 +0000 Subject: [PATCH 09/34] chore: fixed IT tests --- .../cloud/storage/MultipartUploadHttpRequestManager.java | 5 ++--- .../main/java/com/google/cloud/storage/XmlObjectParser.java | 2 ++ .../multipartupload/model/CreateMultipartUploadRequest.java | 2 ++ .../storage/ITMultipartUploadHttpRequestManagerTest.java | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index 809861db2a..78e5b19520 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -53,8 +53,7 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest( HttpRequest httpRequest = requestFactory.buildPostRequest( - new GenericUrl(createUri), - new ByteArrayContent(request.getContentType(), new byte[0])); + new GenericUrl(createUri), new ByteArrayContent(request.getContentType(), new byte[0])); httpRequest.getHeaders().setContentType(request.getContentType()); for (Map.Entry entry : getExtensionHeadersForCreateMultipartUpload(request, options).entrySet()) { @@ -71,7 +70,7 @@ private Map getExtensionHeadersForCreateMultipartUpload( if (request.getCannedAcl() != null) { extensionHeaders.put("x-goog-acl", request.getCannedAcl().toString()); } - //TODO(shreyassinha) Add encoding for x-goog-meta-* headers + // TODO(shreyassinha) Add encoding for x-goog-meta-* headers if (request.getMetadata() != null) { for (Map.Entry entry : request.getMetadata().entrySet()) { if (entry.getKey() != null || entry.getValue() != null) { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java index 5ab9712141..6893f53944 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.util.ObjectParser; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -26,6 +27,7 @@ final class XmlObjectParser implements ObjectParser { private final XmlMapper xmlMapper; + @VisibleForTesting public XmlObjectParser(XmlMapper xmlMapper) { this.xmlMapper = xmlMapper; } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index ce33c90249..722b140c24 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -67,6 +67,7 @@ public String bucket() { /** * Returns the name of the object. * + * @see Object Naming * @return The object name */ public String key() { @@ -234,6 +235,7 @@ public Builder bucket(String bucket) { /** * The name of the object. * + * @see Object Naming * @param key The object name * @return this builder */ diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index cceb908fc5..ff8ba131a0 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -21,8 +21,8 @@ import static io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus.OK; import static org.junit.Assert.assertThrows; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.gson.GsonFactory; import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; import com.google.cloud.storage.it.runner.StorageITRunner; @@ -61,7 +61,7 @@ public final class ITMultipartUploadHttpRequestManagerTest { public void setUp() throws Exception { multipartUploadHttpRequestManager = new MultipartUploadHttpRequestManager( - transport.createRequestFactory(), new JsonObjectParser(gson)); + transport.createRequestFactory(), new XmlObjectParser(new XmlMapper())); httpStorageOptions = HttpStorageOptions.newBuilder().setProjectId("test-project").build(); } From 86a9ab98728ac9c83fda0f6a3ca8538cfafd1f8c Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 17:02:36 +0000 Subject: [PATCH 10/34] removing mockito conifg no longer required --- .../resources/mockito-extensions/org.mockito.plugins.MockMaker | 1 - 1 file changed, 1 deletion(-) delete mode 100644 google-cloud-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/google-cloud-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/google-cloud-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker deleted file mode 100644 index ca6ee9cea8..0000000000 --- a/google-cloud-storage/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker +++ /dev/null @@ -1 +0,0 @@ -mock-maker-inline \ No newline at end of file From 1863d0219b65fbb32ff012214bb44655b1971b62 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 17:21:51 +0000 Subject: [PATCH 11/34] chore: added missing library --- google-cloud-storage/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 855a1f999a..d043318d07 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -23,6 +23,10 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml + + com.fasterxml.jackson.core + jackson-databind + com.google.guava guava From a35e4d6ddfd866b03b05819839ba2a634d67c00d Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 17:49:13 +0000 Subject: [PATCH 12/34] feat: Adding API for ListParts --- .../model/ListPartsRequest.java | 194 ++++++++++++++++++ .../model/ListPartsResponse.java | 194 ++++++++++++++++++ .../storage/multipartupload/model/Part.java | 179 ++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java new file mode 100644 index 0000000000..a9c71b7626 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java @@ -0,0 +1,194 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** + * Represents a request to list the parts of a multipart upload. + */ +public class ListPartsRequest { + private final String bucket; + + private final String key; + + private final String uploadId; + + private final Integer maxParts; + + private final Integer partNumberMarker; + + private ListPartsRequest(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.uploadId = builder.uploadId; + this.maxParts = builder.maxParts; + this.partNumberMarker = builder.partNumberMarker; + } + + /** + * Returns the bucket name. + * @return the bucket name. + */ + public String bucket() { + return bucket; + } + + /** + * Returns the object name. + * @return the object name. + */ + public String key() { + return key; + } + + /** + * Returns the upload ID. + * @return the upload ID. + */ + public String uploadId() { + return uploadId; + } + + /** + * Returns the maximum number of parts to return. + * @return the maximum number of parts to return. + */ + public Integer getMaxParts() { + return maxParts; + } + + /** + * Returns the part number marker. + * @return the part number marker. + */ + public Integer getPartNumberMarker() { + return partNumberMarker; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ListPartsRequest)) { + return false; + } + ListPartsRequest that = (ListPartsRequest) o; + return Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && Objects.equals(uploadId, that.uploadId) + && Objects.equals(maxParts, that.maxParts) + && Objects.equals(partNumberMarker, that.partNumberMarker); + } + + @Override + public int hashCode() { + return Objects.hash(bucket, key, uploadId, maxParts, partNumberMarker); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket) + .add("key", key) + .add("uploadId", uploadId) + .add("maxParts", maxParts) + .add("partNumberMarker", partNumberMarker) + .toString(); + } + + /** + * Returns a new builder for this class. + * @return a new builder for this class. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link ListPartsRequest}. + */ + public static class Builder { + private String bucket; + private String key; + private String uploadId; + private Integer maxParts; + private Integer partNumberMarker; + + private Builder() {} + + /** + * Sets the bucket name. + * @param bucket the bucket name. + * @return this builder. + */ + public Builder bucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the object name. + * @param key the object name. + * @return this builder. + */ + public Builder key(String key) { + this.key = key; + return this; + } + + /** + * Sets the upload ID. + * @param uploadId the upload ID. + * @return this builder. + */ + public Builder uploadId(String uploadId) { + this.uploadId = uploadId; + return this; + } + + /** + * Sets the maximum number of parts to return. + * @param maxParts the maximum number of parts to return. + * @return this builder. + */ + public Builder maxParts(Integer maxParts) { + this.maxParts = maxParts; + return this; + } + + /** + * Sets the part number marker. + * @param partNumberMarker the part number marker. + * @return this builder. + */ + public Builder partNumberMarker(Integer partNumberMarker) { + this.partNumberMarker = partNumberMarker; + return this; + } + + /** + * Builds a new {@link ListPartsRequest} object. + * @return a new {@link ListPartsRequest} object. + */ + public ListPartsRequest build() { + return new ListPartsRequest(this); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java new file mode 100644 index 0000000000..8ddd8b6d4e --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -0,0 +1,194 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.google.common.base.MoreObjects; +import java.util.List; +import java.util.Objects; + +/** + * Represents a response to a list parts request. + */ +public final class ListPartsResponse { + + @JacksonXmlProperty(localName = "Bucket") + private String bucket; + + @JacksonXmlProperty(localName = "Key") + private String key; + + @JacksonXmlProperty(localName = "UploadId") + private String uploadId; + + @JacksonXmlProperty(localName = "PartNumberMarker") + private Integer partNumberMarker; + + @JacksonXmlProperty(localName = "NextPartNumberMarker") + private Integer nextPartNumberMarker; + + @JacksonXmlProperty(localName = "MaxParts") + private Integer maxParts; + + @JsonAlias("truncated") // S3 returns "truncated", GCS returns "IsTruncated" + @JacksonXmlProperty(localName = "IsTruncated") + private boolean isTruncated; + + @JacksonXmlProperty(localName = "Owner") + private String owner; + + @JacksonXmlProperty(localName = "StorageClass") + private String storageClass; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "Part") + private List parts; + + /** + * Returns the bucket name. + * @return the bucket name. + */ + public String getBucket() { + return bucket; + } + + /** + * Returns the object name. + * @return the object name. + */ + public String getKey() { + return key; + } + + /** + * Returns the upload ID. + * @return the upload ID. + */ + public String getUploadId() { + return uploadId; + } + + /** + * Returns the part number marker. + * @return the part number marker. + */ + public Integer getPartNumberMarker() { + return partNumberMarker; + } + + /** + * Returns the next part number marker. + * @return the next part number marker. + */ + public Integer getNextPartNumberMarker() { + return nextPartNumberMarker; + } + + /** + * Returns the maximum number of parts to return. + * @return the maximum number of parts to return. + */ + public Integer getMaxParts() { + return maxParts; + } + + /** + * Returns true if the response is truncated. + * @return true if the response is truncated. + */ + public boolean isTruncated() { + return isTruncated; + } + + /** + * Returns the owner of the object. + * @return the owner of the object. + */ + public String getOwner() { + return owner; + } + + /** + * Returns the storage class of the object. + * @return the storage class of the object. + */ + public String getStorageClass() { + return storageClass; + } + + /** + * Returns the list of parts. + * @return the list of parts. + */ + public List getParts() { + return parts; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ListPartsResponse)) { + return false; + } + ListPartsResponse that = (ListPartsResponse) o; + return Objects.equals(bucket, that.bucket) + && Objects.equals(key, that.key) + && Objects.equals(uploadId, that.uploadId) + && Objects.equals(partNumberMarker, that.partNumberMarker) + && Objects.equals(nextPartNumberMarker, that.nextPartNumberMarker) + && Objects.equals(maxParts, that.maxParts) + && Objects.equals(isTruncated, that.isTruncated) + && Objects.equals(owner, that.owner) + && Objects.equals(storageClass, that.storageClass) + && Objects.equals(parts, that.parts); + } + + @Override + public int hashCode() { + return Objects.hash( + bucket, + key, + uploadId, + partNumberMarker, + nextPartNumberMarker, + maxParts, + isTruncated, + owner, + storageClass, + parts); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("bucket", bucket) + .add("key", key) + .add("uploadId", uploadId) + .add("partNumberMarker", partNumberMarker) + .add("nextPartNumberMarker", nextPartNumberMarker) + .add("maxParts", maxParts) + .add("isTruncated", isTruncated) + .add("owner", owner) + .add("storageClass", storageClass) + .add("parts", parts) + .toString(); + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java new file mode 100644 index 0000000000..271d4455a3 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java @@ -0,0 +1,179 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** + * Represents a part of a multipart upload. + */ +public final class Part { + + @JacksonXmlProperty(localName = "PartNumber") + private int partNumber; + + @JacksonXmlProperty(localName = "ETag") + private String eTag; + + @JacksonXmlProperty(localName = "Size") + private long size; + + @JacksonXmlProperty(localName = "LastModified") + private String lastModified; + + // for jackson + private Part() {} + + private Part(Builder builder) { + this.partNumber = builder.partNumber; + this.eTag = builder.eTag; + this.size = builder.size; + this.lastModified = builder.lastModified; + } + + /** + * Returns the part number. + * @return the part number. + */ + public int partNumber() { + return partNumber; + } + + /** + * Returns the ETag of the part. + * @return the ETag of the part. + */ + public String eTag() { + return eTag; + } + + /** + * Returns the size of the part. + * @return the size of the part. + */ + public long size() { + return size; + } + + /** + * Returns the last modified time of the part. + * @return the last modified time of the part. + */ + public String lastModified() { + return lastModified; + } + + /** + * Returns a new builder for this class. + * @return a new builder for this class. + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Part)) { + return false; + } + Part that = (Part) o; + return Objects.equals(partNumber, that.partNumber) + && Objects.equals(eTag, that.eTag) + && Objects.equals(size, that.size) + && Objects.equals(lastModified, that.lastModified); + } + + @Override + public int hashCode() { + return Objects.hash(partNumber, eTag, size, lastModified); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("partNumber", partNumber) + .add("eTag", eTag) + .add("size", size) + .add("lastModified", lastModified) + .toString(); + } + + /** + * A builder for {@link Part}. + */ + public static final class Builder { + private int partNumber; + private String eTag; + private long size; + private String lastModified; + + private Builder() {} + + /** + * Sets the part number. + * @param partNumber the part number. + * @return this builder. + */ + public Builder partNumber(int partNumber) { + this.partNumber = partNumber; + return this; + } + + /** + * Sets the ETag of the part. + * @param eTag the ETag of the part. + * @return this builder. + */ + public Builder eTag(String eTag) { + this.eTag = eTag; + return this; + } + + /** + * Sets the size of the part. + * @param size the size of the part. + * @return this builder. + */ + public Builder size(long size) { + this.size = size; + return this; + } + + /** + * Sets the last modified time of the part. + * @param lastModified the last modified time of the part. + * @return this builder. + */ + public Builder lastModified(String lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * Builds a new {@link Part} object. + * @return a new {@link Part} object. + */ + public Part build() { + return new Part(this); + } + } +} \ No newline at end of file From 07afc4e4782e960f7492553609161b4fe63dada6 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 18:23:45 +0000 Subject: [PATCH 13/34] chore: adding logic for list parts. --- .../cloud/storage/MultipartUploadClient.java | 25 +++++ .../storage/MultipartUploadClientImpl.java | 14 +++ .../MultipartUploadHttpRequestManager.java | 27 ++++++ .../model/ListPartsRequest.java | 20 ++-- .../model/ListPartsResponse.java | 14 ++- .../storage/multipartupload/model/Part.java | 20 ++-- ...MultipartUploadHttpRequestManagerTest.java | 93 +++++++++++++++++++ 7 files changed, 197 insertions(+), 16 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 0d1a560034..fdd53b8f8d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -20,6 +20,8 @@ import com.google.api.core.InternalExtensionOnly; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ListPartsRequest; +import com.google.cloud.storage.multipartupload.model.ListPartsResponse; import java.io.IOException; import java.net.URI; @@ -37,10 +39,33 @@ public abstract class MultipartUploadClient { MultipartUploadClient() {} + /** + * Creates a new multipart upload. + * + * @param request The request object containing the details for creating the multipart upload. + * @return A {@link CreateMultipartUploadResponse} object containing the upload ID. + * @throws IOException if an I/O error occurs. + */ @BetaApi public abstract CreateMultipartUploadResponse createMultipartUpload( CreateMultipartUploadRequest request) throws IOException; + /** + * Lists the parts that have been uploaded for a specific multipart upload. + * + * @param listPartsRequest The request object containing the details for listing the parts. + * @return A {@link ListPartsResponse} object containing the list of parts. + * @throws IOException if an I/O error occurs. + */ + @BetaApi + public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest) throws IOException; + + /** + * Creates a new instance of {@link MultipartUploadClient}. + * + * @param config The configuration for the client. + * @return A new {@link MultipartUploadClient} instance. + */ @BetaApi public static MultipartUploadClient create(MultipartUploadSettings config) { HttpStorageOptions options = config.getOptions(); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index 8c333455a7..9c52aa441d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -18,9 +18,12 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.http.HttpRequestFactory; import com.google.api.core.BetaApi; +import com.google.cloud.storage.Conversions.Decoder; import com.google.cloud.storage.Retrying.Retrier; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ListPartsRequest; +import com.google.cloud.storage.multipartupload.model.ListPartsResponse; import java.io.IOException; import java.net.URI; @@ -45,9 +48,20 @@ final class MultipartUploadClientImpl extends MultipartUploadClient { this.uri = uri; } + @Override @BetaApi public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) throws IOException { return httpRequestManager.sendCreateMultipartUploadRequest(uri, request, options); } + + @Override + @BetaApi + public ListPartsResponse listParts(ListPartsRequest request) throws IOException { + + return retrier.run( + Retrying.alwaysRetry(), + () -> httpRequestManager.sendListPartsRequest(uri, request, options), + Decoder.identity()); + } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index 78e5b19520..ec382add6a 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -22,6 +22,8 @@ import com.google.api.client.util.ObjectParser; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ListPartsRequest; +import com.google.cloud.storage.multipartupload.model.ListPartsResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; @@ -64,6 +66,31 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest( return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class); } + public ListPartsResponse sendListPartsRequest( + URI uri, ListPartsRequest request, HttpStorageOptions options) throws IOException { + + String encodedBucket = encode(request.bucket()); + String encodedKey = encode(request.key()); + String resourcePath = "/" + encodedBucket + "/" + encodedKey; + String queryString = "?uploadId=" + encode(request.uploadId()); + + if (request.getMaxParts() != null) { + queryString += "&max-parts=" + request.getMaxParts(); + } + if (request.getPartNumberMarker() != null) { + queryString += "&part-number-marker=" + request.getPartNumberMarker(); + } + String listUri = uri.toString() + resourcePath + queryString; + Map extensionHeaders = getGenericExtensionHeader(options); + HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri)); + for (Map.Entry entry : extensionHeaders.entrySet()) { + httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); + } + httpRequest.setParser(objectParser); + httpRequest.setThrowExceptionOnExecuteError(true); + return httpRequest.execute().parseAs(ListPartsResponse.class); + } + private Map getExtensionHeadersForCreateMultipartUpload( CreateMultipartUploadRequest request, HttpStorageOptions options) { Map extensionHeaders = getGenericExtensionHeader(options); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java index a9c71b7626..92625987fe 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java @@ -19,9 +19,7 @@ import com.google.common.base.MoreObjects; import java.util.Objects; -/** - * Represents a request to list the parts of a multipart upload. - */ +/** Represents a request to list the parts of a multipart upload. */ public class ListPartsRequest { private final String bucket; @@ -43,6 +41,7 @@ private ListPartsRequest(Builder builder) { /** * Returns the bucket name. + * * @return the bucket name. */ public String bucket() { @@ -51,6 +50,7 @@ public String bucket() { /** * Returns the object name. + * * @return the object name. */ public String key() { @@ -59,6 +59,7 @@ public String key() { /** * Returns the upload ID. + * * @return the upload ID. */ public String uploadId() { @@ -67,6 +68,7 @@ public String uploadId() { /** * Returns the maximum number of parts to return. + * * @return the maximum number of parts to return. */ public Integer getMaxParts() { @@ -75,6 +77,7 @@ public Integer getMaxParts() { /** * Returns the part number marker. + * * @return the part number marker. */ public Integer getPartNumberMarker() { @@ -115,15 +118,14 @@ public String toString() { /** * Returns a new builder for this class. + * * @return a new builder for this class. */ public static Builder builder() { return new Builder(); } - /** - * A builder for {@link ListPartsRequest}. - */ + /** A builder for {@link ListPartsRequest}. */ public static class Builder { private String bucket; private String key; @@ -135,6 +137,7 @@ private Builder() {} /** * Sets the bucket name. + * * @param bucket the bucket name. * @return this builder. */ @@ -145,6 +148,7 @@ public Builder bucket(String bucket) { /** * Sets the object name. + * * @param key the object name. * @return this builder. */ @@ -155,6 +159,7 @@ public Builder key(String key) { /** * Sets the upload ID. + * * @param uploadId the upload ID. * @return this builder. */ @@ -165,6 +170,7 @@ public Builder uploadId(String uploadId) { /** * Sets the maximum number of parts to return. + * * @param maxParts the maximum number of parts to return. * @return this builder. */ @@ -175,6 +181,7 @@ public Builder maxParts(Integer maxParts) { /** * Sets the part number marker. + * * @param partNumberMarker the part number marker. * @return this builder. */ @@ -185,6 +192,7 @@ public Builder partNumberMarker(Integer partNumberMarker) { /** * Builds a new {@link ListPartsRequest} object. + * * @return a new {@link ListPartsRequest} object. */ public ListPartsRequest build() { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 8ddd8b6d4e..4b11c0be79 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -23,9 +23,7 @@ import java.util.List; import java.util.Objects; -/** - * Represents a response to a list parts request. - */ +/** Represents a response to a list parts request. */ public final class ListPartsResponse { @JacksonXmlProperty(localName = "Bucket") @@ -62,6 +60,7 @@ public final class ListPartsResponse { /** * Returns the bucket name. + * * @return the bucket name. */ public String getBucket() { @@ -70,6 +69,7 @@ public String getBucket() { /** * Returns the object name. + * * @return the object name. */ public String getKey() { @@ -78,6 +78,7 @@ public String getKey() { /** * Returns the upload ID. + * * @return the upload ID. */ public String getUploadId() { @@ -86,6 +87,7 @@ public String getUploadId() { /** * Returns the part number marker. + * * @return the part number marker. */ public Integer getPartNumberMarker() { @@ -94,6 +96,7 @@ public Integer getPartNumberMarker() { /** * Returns the next part number marker. + * * @return the next part number marker. */ public Integer getNextPartNumberMarker() { @@ -102,6 +105,7 @@ public Integer getNextPartNumberMarker() { /** * Returns the maximum number of parts to return. + * * @return the maximum number of parts to return. */ public Integer getMaxParts() { @@ -110,6 +114,7 @@ public Integer getMaxParts() { /** * Returns true if the response is truncated. + * * @return true if the response is truncated. */ public boolean isTruncated() { @@ -118,6 +123,7 @@ public boolean isTruncated() { /** * Returns the owner of the object. + * * @return the owner of the object. */ public String getOwner() { @@ -126,6 +132,7 @@ public String getOwner() { /** * Returns the storage class of the object. + * * @return the storage class of the object. */ public String getStorageClass() { @@ -134,6 +141,7 @@ public String getStorageClass() { /** * Returns the list of parts. + * * @return the list of parts. */ public List getParts() { diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java index 271d4455a3..47599b2380 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java @@ -20,9 +20,7 @@ import com.google.common.base.MoreObjects; import java.util.Objects; -/** - * Represents a part of a multipart upload. - */ +/** Represents a part of a multipart upload. */ public final class Part { @JacksonXmlProperty(localName = "PartNumber") @@ -49,6 +47,7 @@ private Part(Builder builder) { /** * Returns the part number. + * * @return the part number. */ public int partNumber() { @@ -57,6 +56,7 @@ public int partNumber() { /** * Returns the ETag of the part. + * * @return the ETag of the part. */ public String eTag() { @@ -65,6 +65,7 @@ public String eTag() { /** * Returns the size of the part. + * * @return the size of the part. */ public long size() { @@ -73,6 +74,7 @@ public long size() { /** * Returns the last modified time of the part. + * * @return the last modified time of the part. */ public String lastModified() { @@ -81,6 +83,7 @@ public String lastModified() { /** * Returns a new builder for this class. + * * @return a new builder for this class. */ public static Builder builder() { @@ -117,9 +120,7 @@ public String toString() { .toString(); } - /** - * A builder for {@link Part}. - */ + /** A builder for {@link Part}. */ public static final class Builder { private int partNumber; private String eTag; @@ -130,6 +131,7 @@ private Builder() {} /** * Sets the part number. + * * @param partNumber the part number. * @return this builder. */ @@ -140,6 +142,7 @@ public Builder partNumber(int partNumber) { /** * Sets the ETag of the part. + * * @param eTag the ETag of the part. * @return this builder. */ @@ -150,6 +153,7 @@ public Builder eTag(String eTag) { /** * Sets the size of the part. + * * @param size the size of the part. * @return this builder. */ @@ -160,6 +164,7 @@ public Builder size(long size) { /** * Sets the last modified time of the part. + * * @param lastModified the last modified time of the part. * @return this builder. */ @@ -170,10 +175,11 @@ public Builder lastModified(String lastModified) { /** * Builds a new {@link Part} object. + * * @return a new {@link Part} object. */ public Part build() { return new Part(this); } } -} \ No newline at end of file +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index ff8ba131a0..7d389ba604 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -31,6 +31,9 @@ import com.google.cloud.storage.it.runner.annotations.SingleBackend; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ListPartsRequest; +import com.google.cloud.storage.multipartupload.model.ListPartsResponse; +import com.google.cloud.storage.multipartupload.model.Part; import com.google.common.collect.ImmutableMap; import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; import io.grpc.netty.shaded.io.netty.buffer.Unpooled; @@ -372,4 +375,94 @@ public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { endpoint, request, httpStorageOptions); } } + + @Test + public void sendListPartsRequest_success() throws Exception { + HttpRequestHandler handler = + req -> { + String xmlResponse = + "\n" + + "\n" + + " test-bucket\n" + + " test-key\n" + + " test-upload-id\n" + + " 0\n" + + " 1\n" + + " 1\n" + + " false\n" + + " \n" + + " 1\n" + + " \"etag\"\n" + + " 123\n" + + " 2024-05-08T17:50:00.000Z\n" + + " \n" + + ""; + ByteBuf buf = Unpooled.wrappedBuffer(xmlResponse.getBytes()); + + DefaultFullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); + resp.headers().set(CONTENT_TYPE, "application/xml; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + ListPartsRequest request = + ListPartsRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .maxParts(1) + .partNumberMarker(0) + .build(); + + ListPartsResponse response = + multipartUploadHttpRequestManager.sendListPartsRequest( + endpoint, request, httpStorageOptions); + + assertThat(response).isNotNull(); + assertThat(response.getBucket()).isEqualTo("test-bucket"); + assertThat(response.getKey()).isEqualTo("test-key"); + assertThat(response.getUploadId()).isEqualTo("test-upload-id"); + assertThat(response.getPartNumberMarker()).isEqualTo(0); + assertThat(response.getNextPartNumberMarker()).isEqualTo(1); + assertThat(response.getMaxParts()).isEqualTo(1); + assertThat(response.isTruncated()).isFalse(); + assertThat(response.getParts()).hasSize(1); + Part part = response.getParts().get(0); + assertThat(part.partNumber()).isEqualTo(1); + assertThat(part.eTag()).isEqualTo("\"etag\""); + assertThat(part.size()).isEqualTo(123); + assertThat(part.lastModified()).isEqualTo("2024-05-08T17:50:00.000Z"); + } + } + + @Test + public void sendListPartsRequest_error() throws Exception { + HttpRequestHandler handler = + req -> { + FullHttpResponse resp = + new DefaultFullHttpResponse(req.protocolVersion(), HttpResponseStatus.BAD_REQUEST); + resp.headers().set(CONTENT_TYPE, "text/plain; charset=utf-8"); + return resp; + }; + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + ListPartsRequest request = + ListPartsRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + + StorageException se = + assertThrows( + StorageException.class, + () -> + multipartUploadHttpRequestManager.sendListPartsRequest( + endpoint, request, httpStorageOptions)); + assertThat(se.getCode()).isEqualTo(400); + } + } } From 48e1a2f35847cb182fc7bb46eb80982df78af9cb Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 18:36:34 +0000 Subject: [PATCH 14/34] chore: adding missing final statement --- .../cloud/storage/multipartupload/model/ListPartsRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java index 92625987fe..4294bfb213 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java @@ -20,7 +20,7 @@ import java.util.Objects; /** Represents a request to list the parts of a multipart upload. */ -public class ListPartsRequest { +public final class ListPartsRequest { private final String bucket; private final String key; From 81648dcaf4942ba2a8d41e6a7c040e4dfa87ef02 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 18:46:34 +0000 Subject: [PATCH 15/34] fixing licence number --- .../cloud/storage/XmlObjectParserTest.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java new file mode 100644 index 0000000000..38dab1e530 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2025 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 static org.mockito.Mockito.when; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class XmlObjectParserTest { + + @Mock private XmlMapper xmlMapper; + + private XmlObjectParser xmlObjectParser; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + xmlObjectParser = new XmlObjectParser(xmlMapper); + } + + @Test + public void testParseAndClose() throws IOException { + InputStream in = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + TestXmlObject expected = new TestXmlObject(); + when(xmlMapper.readValue(in, TestXmlObject.class)).thenReturn(expected); + TestXmlObject actual = xmlObjectParser.parseAndClose(in, StandardCharsets.UTF_8, TestXmlObject.class); + assertThat(actual).isSameInstanceAs(expected); + } + + private static class TestXmlObject {} +} From 2a5d1a02b8261c265c2a743a8b9567c108c0e05c Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 18:47:40 +0000 Subject: [PATCH 16/34] fixing lint error --- .../java/com/google/cloud/storage/XmlObjectParserTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java index 38dab1e530..2780309e81 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java @@ -46,7 +46,8 @@ public void testParseAndClose() throws IOException { InputStream in = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); TestXmlObject expected = new TestXmlObject(); when(xmlMapper.readValue(in, TestXmlObject.class)).thenReturn(expected); - TestXmlObject actual = xmlObjectParser.parseAndClose(in, StandardCharsets.UTF_8, TestXmlObject.class); + TestXmlObject actual = + xmlObjectParser.parseAndClose(in, StandardCharsets.UTF_8, TestXmlObject.class); assertThat(actual).isSameInstanceAs(expected); } From 21c5c94a5b4ff787d017fb82957bec3ce32621bc Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 19:05:13 +0000 Subject: [PATCH 17/34] chore: adding beta api annotation. --- .../cloud/storage/multipartupload/model/ListPartsRequest.java | 2 ++ .../cloud/storage/multipartupload/model/ListPartsResponse.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java index 4294bfb213..5c3dcedf76 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java @@ -16,10 +16,12 @@ package com.google.cloud.storage.multipartupload.model; +import com.google.api.core.BetaApi; import com.google.common.base.MoreObjects; import java.util.Objects; /** Represents a request to list the parts of a multipart upload. */ +@BetaApi public final class ListPartsRequest { private final String bucket; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 4b11c0be79..8cdb28d127 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -19,11 +19,13 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.google.api.core.BetaApi; import com.google.common.base.MoreObjects; import java.util.List; import java.util.Objects; /** Represents a response to a list parts request. */ +@BetaApi public final class ListPartsResponse { @JacksonXmlProperty(localName = "Bucket") From fffd81b995d7bb3dbeb99595505fe09a8beb662d Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Wed, 22 Oct 2025 19:59:04 +0000 Subject: [PATCH 18/34] chore: added missing annotation --- google-cloud-storage/pom.xml | 4 ++++ .../cloud/storage/MultipartUploadHttpRequestManager.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index d043318d07..41b1205f9f 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -27,6 +27,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.core + jackson-annotations + com.google.guava guava diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index ec382add6a..f6a1117e1d 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -66,7 +66,7 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest( return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class); } - public ListPartsResponse sendListPartsRequest( + ListPartsResponse sendListPartsRequest( URI uri, ListPartsRequest request, HttpStorageOptions options) throws IOException { String encodedBucket = encode(request.bucket()); From 744db03c0dbab6acdee98f2440dfa3855e8188bc Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Thu, 23 Oct 2025 05:36:03 +0000 Subject: [PATCH 19/34] chore: adding fixes for review comments --- .../google/cloud/storage/XmlObjectParser.java | 16 ++++++++--- .../model/CreateMultipartUploadRequest.java | 1 - .../model}/ObjectLockMode.java | 2 +- ...MultipartUploadHttpRequestManagerTest.java | 27 +++++++++++-------- .../cloud/storage/XmlObjectParserTest.java | 13 +++++++-- 5 files changed, 40 insertions(+), 19 deletions(-) rename google-cloud-storage/src/main/java/com/google/cloud/storage/{ => multipartupload/model}/ObjectLockMode.java (97%) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java index 6893f53944..aae93d4796 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java @@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; import java.nio.charset.Charset; @@ -35,21 +36,28 @@ public XmlObjectParser(XmlMapper xmlMapper) { @Override public T parseAndClose(InputStream in, Charset charset, Class dataClass) throws IOException { - return xmlMapper.readValue(in, dataClass); + return parseAndClose(new InputStreamReader(in, charset), dataClass); } @Override public Object parseAndClose(InputStream in, Charset charset, Type dataType) throws IOException { - return xmlMapper.readValue(in, xmlMapper.getTypeFactory().constructType(dataType)); + throw new UnsupportedOperationException( + "XmlObjectParse#" + + CrossTransportUtils.fmtMethodName( + "parseAndClose", InputStream.class, Charset.class, Type.class)); } @Override public T parseAndClose(Reader reader, Class dataClass) throws IOException { - return xmlMapper.readValue(reader, dataClass); + try (Reader r = reader) { + return xmlMapper.readValue(r, dataClass); + } } @Override public Object parseAndClose(Reader reader, Type dataType) throws IOException { - return xmlMapper.readValue(reader, xmlMapper.getTypeFactory().constructType(dataType)); + throw new UnsupportedOperationException( + "XmlObjectParse#" + + CrossTransportUtils.fmtMethodName("parseAndClose", Reader.class, Type.class)); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index 722b140c24..97418e93fa 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -17,7 +17,6 @@ package com.google.cloud.storage.multipartupload.model; import com.google.api.core.BetaApi; -import com.google.cloud.storage.ObjectLockMode; import com.google.cloud.storage.Storage.PredefinedAcl; import com.google.cloud.storage.StorageClass; import com.google.common.base.MoreObjects; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java similarity index 97% rename from google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java rename to google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java index f08ec2e08f..a058719e1c 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ObjectLockMode.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ObjectLockMode.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.storage; +package com.google.cloud.storage.multipartupload.model; import com.google.api.core.ApiFunction; import com.google.api.core.BetaApi; diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index ff8ba131a0..0189056158 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; +import com.google.cloud.NoCredentials; import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; import com.google.cloud.storage.it.runner.StorageITRunner; import com.google.cloud.storage.it.runner.annotations.Backend; @@ -31,6 +31,7 @@ import com.google.cloud.storage.it.runner.annotations.SingleBackend; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.cloud.storage.multipartupload.model.ObjectLockMode; import com.google.common.collect.ImmutableMap; import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; import io.grpc.netty.shaded.io.netty.buffer.Unpooled; @@ -50,7 +51,7 @@ @SingleBackend(Backend.PROD) @ParallelFriendly public final class ITMultipartUploadHttpRequestManagerTest { - private static final GsonFactory gson = GsonFactory.getDefaultInstance(); + private static final XmlMapper xmlMapper = new XmlMapper(); private static final NetHttpTransport transport = new NetHttpTransport.Builder().build(); private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager; private HttpStorageOptions httpStorageOptions; @@ -62,7 +63,11 @@ public void setUp() throws Exception { multipartUploadHttpRequestManager = new MultipartUploadHttpRequestManager( transport.createRequestFactory(), new XmlObjectParser(new XmlMapper())); - httpStorageOptions = HttpStorageOptions.newBuilder().setProjectId("test-project").build(); + httpStorageOptions = + HttpStorageOptions.newBuilder() + .setProjectId("test-project") + .setCredentials(NoCredentials.getInstance()) + .build(); } @Test @@ -75,7 +80,7 @@ public void sendCreateMultipartUploadRequest_success() throws Exception { .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -143,7 +148,7 @@ public void sendCreateMultipartUploadRequest_withCannedAcl() throws Exception { .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -178,7 +183,7 @@ public void sendCreateMultipartUploadRequest_withMetadata() throws Exception { .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -212,7 +217,7 @@ public void sendCreateMultipartUploadRequest_withStorageClass() throws Exception .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -247,7 +252,7 @@ public void sendCreateMultipartUploadRequest_withKmsKeyName() throws Exception { .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -281,7 +286,7 @@ public void sendCreateMultipartUploadRequest_withObjectLockMode() throws Excepti .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -316,7 +321,7 @@ public void sendCreateMultipartUploadRequest_withObjectLockRetainUntilDate() thr .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -350,7 +355,7 @@ public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { .key("test-key") .uploadId("test-upload-id") .build(); - ByteBuf buf = Unpooled.wrappedBuffer(gson.toByteArray(response)); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(response)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java index 2780309e81..c4acd8c64a 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/XmlObjectParserTest.java @@ -17,13 +17,16 @@ package com.google.cloud.storage; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.nio.charset.StandardCharsets; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -33,19 +36,25 @@ public class XmlObjectParserTest { @Mock private XmlMapper xmlMapper; + private AutoCloseable mocks; private XmlObjectParser xmlObjectParser; @Before public void setUp() { - MockitoAnnotations.initMocks(this); + mocks = MockitoAnnotations.openMocks(this); xmlObjectParser = new XmlObjectParser(xmlMapper); } + @After + public void tearDown() throws Exception { + mocks.close(); + } + @Test public void testParseAndClose() throws IOException { InputStream in = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); TestXmlObject expected = new TestXmlObject(); - when(xmlMapper.readValue(in, TestXmlObject.class)).thenReturn(expected); + when(xmlMapper.readValue(any(Reader.class), any(Class.class))).thenReturn(expected); TestXmlObject actual = xmlObjectParser.parseAndClose(in, StandardCharsets.UTF_8, TestXmlObject.class); assertThat(actual).isSameInstanceAs(expected); From 08b084f4b9ac825e174985c73cb3690367a36586 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Thu, 23 Oct 2025 05:44:38 +0000 Subject: [PATCH 20/34] chore: Adding lint issues. --- .../cloud/storage/ITMultipartUploadHttpRequestManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index ce894ceb7d..61b232c331 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -33,8 +33,8 @@ import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; import com.google.cloud.storage.multipartupload.model.ListPartsRequest; import com.google.cloud.storage.multipartupload.model.ListPartsResponse; -import com.google.cloud.storage.multipartupload.model.Part; import com.google.cloud.storage.multipartupload.model.ObjectLockMode; +import com.google.cloud.storage.multipartupload.model.Part; import com.google.common.collect.ImmutableMap; import io.grpc.netty.shaded.io.netty.buffer.ByteBuf; import io.grpc.netty.shaded.io.netty.buffer.Unpooled; From e565af605ca118749fbda89a4843f05cbcdb93f6 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Thu, 23 Oct 2025 08:21:13 +0000 Subject: [PATCH 21/34] chore: Adding fixes for IT. --- .../ITMultipartUploadHttpRequestManagerTest.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 0189056158..0a5b92fc9b 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.cloud.NoCredentials; import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; @@ -127,13 +128,11 @@ public void sendCreateMultipartUploadRequest_error() throws Exception { .contentType("application/octet-stream") .build(); - StorageException se = - assertThrows( - StorageException.class, - () -> - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions)); - assertThat(se.getCode()).isEqualTo(400); + assertThrows( + HttpResponseException.class, + () -> + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( + endpoint, request, httpStorageOptions)); } } @@ -141,7 +140,7 @@ public void sendCreateMultipartUploadRequest_error() throws Exception { public void sendCreateMultipartUploadRequest_withCannedAcl() throws Exception { HttpRequestHandler handler = req -> { - assertThat(req.headers().get("x-goog-acl")).isEqualTo("authenticatedRead"); + assertThat(req.headers().get("x-goog-acl")).isEqualTo("AUTHENTICATED_READ"); CreateMultipartUploadResponse response = CreateMultipartUploadResponse.builder() .bucket("test-bucket") From 0b378af262d7ae8949aeddbf779e4985b5c6c03f Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Thu, 23 Oct 2025 08:24:24 +0000 Subject: [PATCH 22/34] added test --- .../ITMultipartUploadHttpRequestManagerTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 7f3768f6d7..60411e902c 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -460,13 +460,11 @@ public void sendListPartsRequest_error() throws Exception { .uploadId("test-upload-id") .build(); - StorageException se = - assertThrows( - StorageException.class, - () -> - multipartUploadHttpRequestManager.sendListPartsRequest( - endpoint, request, httpStorageOptions)); - assertThat(se.getCode()).isEqualTo(400); + assertThrows( + HttpResponseException.class, + () -> + multipartUploadHttpRequestManager.sendListPartsRequest( + endpoint, request, httpStorageOptions)); } } } From b8530f75af08e4cf28f3e12d6bc0994e9ad10645 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Fri, 24 Oct 2025 12:58:28 +0000 Subject: [PATCH 23/34] chore: Adding review comments --- .../cloud/storage/MultipartUploadClient.java | 3 +- .../storage/MultipartUploadClientImpl.java | 13 +- .../MultipartUploadHttpRequestManager.java | 115 ++++++++++++------ .../java/com/google/cloud/storage/Utils.java | 5 + ...MultipartUploadHttpRequestManagerTest.java | 55 ++++----- 5 files changed, 112 insertions(+), 79 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 0d1a560034..768eed5455 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -46,8 +46,7 @@ public static MultipartUploadClient create(MultipartUploadSettings config) { HttpStorageOptions options = config.getOptions(); return new MultipartUploadClientImpl( URI.create(options.getHost()), - options.getStorageRpcV1().getStorage().getRequestFactory(), options.createRetrier(), - options); + MultipartUploadHttpRequestManager.createFrom(options)); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index 8c333455a7..ab3a8a0de9 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -15,8 +15,6 @@ */ package com.google.cloud.storage; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import com.google.api.client.http.HttpRequestFactory; import com.google.api.core.BetaApi; import com.google.cloud.storage.Retrying.Retrier; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; @@ -32,15 +30,14 @@ final class MultipartUploadClientImpl extends MultipartUploadClient { private final MultipartUploadHttpRequestManager httpRequestManager; - private final HttpStorageOptions options; private final Retrier retrier; private final URI uri; MultipartUploadClientImpl( - URI uri, HttpRequestFactory requestFactory, Retrier retrier, HttpStorageOptions options) { - this.httpRequestManager = - new MultipartUploadHttpRequestManager(requestFactory, new XmlObjectParser(new XmlMapper())); - this.options = options; + URI uri, + Retrier retrier, + MultipartUploadHttpRequestManager multipartUploadHttpRequestManager) { + this.httpRequestManager = multipartUploadHttpRequestManager; this.retrier = retrier; this.uri = uri; } @@ -48,6 +45,6 @@ final class MultipartUploadClientImpl extends MultipartUploadClient { @BetaApi public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request) throws IOException { - return httpRequestManager.sendCreateMultipartUploadRequest(uri, request, options); + return httpRequestManager.sendCreateMultipartUploadRequest(uri, request); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index 78e5b19520..fc7b181a86 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -15,105 +15,144 @@ */ package com.google.cloud.storage; +import static com.google.cloud.storage.Utils.ifNonNull; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.http.ByteArrayContent; import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.util.ObjectParser; +import com.google.api.gax.core.GaxProperties; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.api.gax.rpc.HeaderProvider; +import com.google.api.services.storage.Storage; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest; import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse; +import com.google.common.base.StandardSystemProperty; +import com.google.common.collect.ImmutableMap; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; final class MultipartUploadHttpRequestManager { private final HttpRequestFactory requestFactory; private final ObjectParser objectParser; + private final HeaderProvider headerProvider; - MultipartUploadHttpRequestManager(HttpRequestFactory requestFactory, ObjectParser objectParser) { + MultipartUploadHttpRequestManager( + HttpRequestFactory requestFactory, ObjectParser objectParser, HeaderProvider headerProvider) { this.requestFactory = requestFactory; this.objectParser = objectParser; + this.headerProvider = headerProvider; } CreateMultipartUploadResponse sendCreateMultipartUploadRequest( - URI uri, CreateMultipartUploadRequest request, HttpStorageOptions options) - throws IOException { + URI uri, CreateMultipartUploadRequest request) throws IOException { - String encodedBucket = encode(request.bucket()); - String encodedKey = encode(request.key()); + String encodedBucket = urlEncode(request.bucket()); + String encodedKey = urlEncode(request.key()); String resourcePath = "/" + encodedBucket + "/" + encodedKey; String createUri = uri.toString() + resourcePath + "?uploads"; HttpRequest httpRequest = requestFactory.buildPostRequest( new GenericUrl(createUri), new ByteArrayContent(request.getContentType(), new byte[0])); - httpRequest.getHeaders().setContentType(request.getContentType()); - for (Map.Entry entry : - getExtensionHeadersForCreateMultipartUpload(request, options).entrySet()) { - httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); - } + httpRequest.getHeaders().putAll(headerProvider.getHeaders()); + addHeadersForCreateMultipartUpload(request, httpRequest.getHeaders()); httpRequest.setParser(objectParser); httpRequest.setThrowExceptionOnExecuteError(true); return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class); } - private Map getExtensionHeadersForCreateMultipartUpload( - CreateMultipartUploadRequest request, HttpStorageOptions options) { - Map extensionHeaders = getGenericExtensionHeader(options); + @SuppressWarnings("DataFlowIssue") + static MultipartUploadHttpRequestManager createFrom(HttpStorageOptions options) { + Storage storage = options.getStorageRpcV1().getStorage(); + ImmutableMap.Builder stableHeaders = + ImmutableMap.builder() + // http-java-client will automatically append its own version to the user-agent + .put("User-Agent", "gcloud-java/" + options.getLibraryVersion()) + .put( + "x-goog-api-client", + String.format( + "gl-java/%s gccl/%s %s/%s", + GaxProperties.getJavaVersion(), + options.getLibraryVersion(), + formatName(StandardSystemProperty.OS_NAME.value()), + formatSemver(StandardSystemProperty.OS_VERSION.value()))); + ifNonNull(options.getProjectId(), pid -> stableHeaders.put("x-goog-user-project", pid)); + return new MultipartUploadHttpRequestManager( + storage.getRequestFactory(), + new XmlObjectParser(new XmlMapper()), + options.getMergedHeaderProvider(FixedHeaderProvider.create(stableHeaders.build()))); + } + + private void addHeadersForCreateMultipartUpload( + CreateMultipartUploadRequest request, HttpHeaders headers) { + // TODO(shreyassinha): add a PredefinedAcl::getXmlEntry with the corresponding value from + // https://cloud.google.com/storage/docs/xml-api/reference-headers#xgoogacl if (request.getCannedAcl() != null) { - extensionHeaders.put("x-goog-acl", request.getCannedAcl().toString()); + headers.put("x-goog-acl", request.getCannedAcl().toString()); } // TODO(shreyassinha) Add encoding for x-goog-meta-* headers if (request.getMetadata() != null) { for (Map.Entry entry : request.getMetadata().entrySet()) { if (entry.getKey() != null || entry.getValue() != null) { - extensionHeaders.put("x-goog-meta-" + entry.getKey(), entry.getValue()); + headers.put("x-goog-meta-" + entry.getKey(), entry.getValue()); } } } if (request.getStorageClass() != null) { - extensionHeaders.put("x-goog-storage-class", request.getStorageClass().toString()); + headers.put("x-goog-storage-class", request.getStorageClass().toString()); } if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) { - extensionHeaders.put("x-goog-encryption-kms-key-name", request.getKmsKeyName()); + headers.put("x-goog-encryption-kms-key-name", request.getKmsKeyName()); } if (request.getObjectLockMode() != null) { - extensionHeaders.put("x-goog-object-lock-mode", request.getObjectLockMode().toString()); + headers.put("x-goog-object-lock-mode", request.getObjectLockMode().toString()); } if (request.getObjectLockRetainUntilDate() != null) { - extensionHeaders.put( + headers.put( "x-goog-object-lock-retain-until-date", - toRfc3339String(request.getObjectLockRetainUntilDate())); + Utils.offsetDateTimeRfc3339Codec.encode(request.getObjectLockRetainUntilDate())); } if (request.getCustomTime() != null) { - extensionHeaders.put("x-goog-custom-time", toRfc3339String(request.getCustomTime())); + headers.put( + "x-goog-custom-time", Utils.offsetDateTimeRfc3339Codec.encode(request.getCustomTime())); } - return extensionHeaders; } - private Map getGenericExtensionHeader(HttpStorageOptions options) { - Map extensionHeaders = new HashMap<>(); - if (options.getClientLibToken() != null) { - extensionHeaders.put("x-goog-api-client", options.getClientLibToken()); - } - if (options.getProjectId() != null) { - extensionHeaders.put("x-goog-user-project", options.getProjectId()); - } - return extensionHeaders; + private static String urlEncode(String value) throws UnsupportedEncodingException { + return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); } - private String encode(String value) throws UnsupportedEncodingException { - return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); + private static String formatName(String name) { + // Only lowercase letters, digits, and "-" are allowed + return name.toLowerCase().replaceAll("[^\\w\\d\\-]", "-"); + } + + private static String formatSemver(String version) { + return formatSemver(version, version); } - private String toRfc3339String(OffsetDateTime dateTime) { - return DateTimeFormatter.ISO_INSTANT.format(dateTime); + private static String formatSemver(String version, String defaultValue) { + if (version == null) { + return null; + } + + // Take only the semver version: x.y.z-a_b_c -> x.y.z + Matcher m = Pattern.compile("(\\d+\\.\\d+\\.\\d+).*").matcher(version); + if (m.find()) { + return m.group(1); + } else { + return defaultValue; + } } } 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 eece2fe79d..90d6122c9a 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 @@ -84,6 +84,11 @@ final class Utils { static final Codec durationSecondsCodec = Codec.of(Duration::getSeconds, Duration::ofSeconds); + static final Codec offsetDateTimeRfc3339Codec = + Codec.of( + RFC_3339_DATE_TIME_FORMATTER::format, + s -> OffsetDateTime.parse(s, RFC_3339_DATE_TIME_FORMATTER)); + @VisibleForTesting static final Codec dateTimeCodec = Codec.of( diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 0a5b92fc9b..72625d0baf 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.javanet.NetHttpTransport; import com.google.cloud.NoCredentials; import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; import com.google.cloud.storage.it.runner.StorageITRunner; @@ -53,22 +52,18 @@ @ParallelFriendly public final class ITMultipartUploadHttpRequestManagerTest { private static final XmlMapper xmlMapper = new XmlMapper(); - private static final NetHttpTransport transport = new NetHttpTransport.Builder().build(); private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager; - private HttpStorageOptions httpStorageOptions; - @Rule public final TemporaryFolder temp = new TemporaryFolder(); @Before public void setUp() throws Exception { - multipartUploadHttpRequestManager = - new MultipartUploadHttpRequestManager( - transport.createRequestFactory(), new XmlObjectParser(new XmlMapper())); - httpStorageOptions = + HttpStorageOptions httpStorageOptions = HttpStorageOptions.newBuilder() .setProjectId("test-project") .setCredentials(NoCredentials.getInstance()) .build(); + multipartUploadHttpRequestManager = + MultipartUploadHttpRequestManager.createFrom(httpStorageOptions); } @Test @@ -99,8 +94,7 @@ public void sendCreateMultipartUploadRequest_success() throws Exception { .build(); CreateMultipartUploadResponse response = - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); assertThat(response).isNotNull(); assertThat(response.bucket()).isEqualTo("test-bucket"); @@ -132,7 +126,7 @@ public void sendCreateMultipartUploadRequest_error() throws Exception { HttpResponseException.class, () -> multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions)); + endpoint, request)); } } @@ -165,8 +159,7 @@ public void sendCreateMultipartUploadRequest_withCannedAcl() throws Exception { .cannedAcl(Storage.PredefinedAcl.AUTHENTICATED_READ) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @@ -200,8 +193,7 @@ public void sendCreateMultipartUploadRequest_withMetadata() throws Exception { .metadata(ImmutableMap.of("key1", "value1", "key2", "value2")) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @@ -234,8 +226,7 @@ public void sendCreateMultipartUploadRequest_withStorageClass() throws Exception .storageClass(StorageClass.ARCHIVE) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @@ -269,8 +260,7 @@ public void sendCreateMultipartUploadRequest_withKmsKeyName() throws Exception { .kmsKeyName("projects/p/locations/l/keyRings/r/cryptoKeys/k") .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @@ -303,17 +293,19 @@ public void sendCreateMultipartUploadRequest_withObjectLockMode() throws Excepti .objectLockMode(ObjectLockMode.GOVERNANCE) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @Test public void sendCreateMultipartUploadRequest_withObjectLockRetainUntilDate() throws Exception { + OffsetDateTime retainUtil = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); HttpRequestHandler handler = req -> { - assertThat(req.headers().get("x-goog-object-lock-retain-until-date")) - .isEqualTo("2024-01-01T00:00:00Z"); + OffsetDateTime actual = + Utils.offsetDateTimeRfc3339Codec.decode( + req.headers().get("x-goog-object-lock-retain-until-date")); + assertThat(actual).isEqualTo(retainUtil); CreateMultipartUploadResponse response = CreateMultipartUploadResponse.builder() .bucket("test-bucket") @@ -335,19 +327,21 @@ public void sendCreateMultipartUploadRequest_withObjectLockRetainUntilDate() thr .bucket("test-bucket") .key("test-key") .contentType("application/octet-stream") - .objectLockRetainUntilDate(OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .objectLockRetainUntilDate(retainUtil) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } @Test public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { + OffsetDateTime customTime = OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); HttpRequestHandler handler = req -> { - assertThat(req.headers().get("x-goog-custom-time")).isEqualTo("2024-01-01T00:00:00Z"); + OffsetDateTime actual = + Utils.offsetDateTimeRfc3339Codec.decode(req.headers().get("x-goog-custom-time")); + assertThat(actual).isEqualTo(customTime); CreateMultipartUploadResponse response = CreateMultipartUploadResponse.builder() .bucket("test-bucket") @@ -369,11 +363,10 @@ public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { .bucket("test-bucket") .key("test-key") .contentType("application/octet-stream") - .customTime(OffsetDateTime.of(2024, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .customTime(customTime) .build(); - multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest( - endpoint, request, httpStorageOptions); + multipartUploadHttpRequestManager.sendCreateMultipartUploadRequest(endpoint, request); } } } From 956c59505b028924d78e75d952cdecb1461e7d3c Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Fri, 24 Oct 2025 13:03:50 +0000 Subject: [PATCH 24/34] Adding review comments --- google-cloud-storage/pom.xml | 4 ---- .../com/google/cloud/storage/MultipartUploadClientImpl.java | 2 +- .../cloud/storage/MultipartUploadHttpRequestManager.java | 2 +- .../main/java/com/google/cloud/storage/XmlObjectParser.java | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index d043318d07..855a1f999a 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -23,10 +23,6 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml - - com.fasterxml.jackson.core - jackson-databind - com.google.guava guava diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index ab3a8a0de9..853186e775 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index fc7b181a86..eb8db6cc60 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java index aae93d4796..27c91fef48 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From b8437d6d7c35568c61b135f46047af1160e4067a Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Fri, 24 Oct 2025 13:34:39 +0000 Subject: [PATCH 25/34] chore: Adding missing BetaApi annotation. --- .../multipartupload/model/CreateMultipartUploadRequest.java | 2 ++ .../multipartupload/model/CreateMultipartUploadResponse.java | 1 + 2 files changed, 3 insertions(+) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index 97418e93fa..dc1545d1f4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -206,6 +206,8 @@ public static Builder builder() { return new Builder(); } + + @BetaApi public static final class Builder { private String bucket; private String key; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java index 35d2b2c171..5487dd7316 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -113,6 +113,7 @@ public static Builder builder() { } /** A builder for {@link CreateMultipartUploadResponse} objects. */ + @BetaApi public static final class Builder { private String bucket; private String key; From 1ec5eadef483f7bcdd3b64dc42f3825fc1e36706 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Fri, 24 Oct 2025 13:46:42 +0000 Subject: [PATCH 26/34] chore:fixing lint error. --- .../multipartupload/model/CreateMultipartUploadRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java index dc1545d1f4..44161ad160 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java @@ -206,7 +206,6 @@ public static Builder builder() { return new Builder(); } - @BetaApi public static final class Builder { private String bucket; From 5c7c6ca24fd72f817b2b1d8f980e8dfaed3cda67 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Fri, 24 Oct 2025 14:18:51 +0000 Subject: [PATCH 27/34] chore: merged feature 2 into feature 3 --- .../cloud/storage/MultipartUploadClientImpl.java | 2 +- .../storage/MultipartUploadHttpRequestManager.java | 13 +++++-------- .../ITMultipartUploadHttpRequestManagerTest.java | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index 1b75e697b8..ae09fb2b15 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -58,7 +58,7 @@ public ListPartsResponse listParts(ListPartsRequest request) throws IOException return retrier.run( Retrying.alwaysRetry(), - () -> httpRequestManager.sendListPartsRequest(uri, request, options), + () -> httpRequestManager.sendListPartsRequest(uri, request), Decoder.identity()); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index db285ec65e..7d6441b5c8 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -75,12 +75,12 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest( } ListPartsResponse sendListPartsRequest( - URI uri, ListPartsRequest request, HttpStorageOptions options) throws IOException { + URI uri, ListPartsRequest request) throws IOException { - String encodedBucket = encode(request.bucket()); - String encodedKey = encode(request.key()); + String encodedBucket = urlEncode(request.bucket()); + String encodedKey = urlEncode(request.key()); String resourcePath = "/" + encodedBucket + "/" + encodedKey; - String queryString = "?uploadId=" + encode(request.uploadId()); + String queryString = "?uploadId=" + urlEncode(request.uploadId()); if (request.getMaxParts() != null) { queryString += "&max-parts=" + request.getMaxParts(); @@ -89,11 +89,8 @@ ListPartsResponse sendListPartsRequest( queryString += "&part-number-marker=" + request.getPartNumberMarker(); } String listUri = uri.toString() + resourcePath + queryString; - Map extensionHeaders = getGenericExtensionHeader(options); HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri)); - for (Map.Entry entry : extensionHeaders.entrySet()) { - httpRequest.getHeaders().set(entry.getKey(), entry.getValue()); - } + httpRequest.getHeaders().putAll(headerProvider.getHeaders()); httpRequest.setParser(objectParser); httpRequest.setThrowExceptionOnExecuteError(true); return httpRequest.execute().parseAs(ListPartsResponse.class); diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 86b186c544..182edb2da1 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -415,7 +415,7 @@ public void sendListPartsRequest_success() throws Exception { ListPartsResponse response = multipartUploadHttpRequestManager.sendListPartsRequest( - endpoint, request, httpStorageOptions); + endpoint, request); assertThat(response).isNotNull(); assertThat(response.getBucket()).isEqualTo("test-bucket"); @@ -457,7 +457,7 @@ public void sendListPartsRequest_error() throws Exception { HttpResponseException.class, () -> multipartUploadHttpRequestManager.sendListPartsRequest( - endpoint, request, httpStorageOptions)); + endpoint, request)); } } } From daaad24eb5238e387a32c322636d5baec0014ec2 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Sun, 26 Oct 2025 03:58:23 +0000 Subject: [PATCH 28/34] choere: coverted Integer to int. --- .../MultipartUploadHttpRequestManager.java | 3 +- .../model/CreateMultipartUploadResponse.java | 8 +- .../model/ListPartsResponse.java | 169 +++++++++++++++++- ...MultipartUploadHttpRequestManagerTest.java | 7 +- 4 files changed, 169 insertions(+), 18 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java index 7d6441b5c8..a8f23a1b04 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java @@ -74,8 +74,7 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest( return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class); } - ListPartsResponse sendListPartsRequest( - URI uri, ListPartsRequest request) throws IOException { + ListPartsResponse sendListPartsRequest(URI uri, ListPartsRequest request) throws IOException { String encodedBucket = urlEncode(request.bucket()); String encodedKey = urlEncode(request.key()); diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java index 5487dd7316..d5c933de9f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -31,13 +31,13 @@ public final class CreateMultipartUploadResponse { @JacksonXmlProperty(localName = "Bucket") - private String bucket; + private final String bucket; @JacksonXmlProperty(localName = "Key") - private String key; + private final String key; @JacksonXmlProperty(localName = "UploadId") - private String uploadId; + private final String uploadId; private CreateMultipartUploadResponse(Builder builder) { this.bucket = builder.bucket; @@ -45,8 +45,6 @@ private CreateMultipartUploadResponse(Builder builder) { this.uploadId = builder.uploadId; } - private CreateMultipartUploadResponse() {} - /** * Returns the name of the bucket where the multipart upload was initiated. * diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 8cdb28d127..6be2f186c4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -38,13 +38,13 @@ public final class ListPartsResponse { private String uploadId; @JacksonXmlProperty(localName = "PartNumberMarker") - private Integer partNumberMarker; + private int partNumberMarker; @JacksonXmlProperty(localName = "NextPartNumberMarker") - private Integer nextPartNumberMarker; + private int nextPartNumberMarker; @JacksonXmlProperty(localName = "MaxParts") - private Integer maxParts; + private int maxParts; @JsonAlias("truncated") // S3 returns "truncated", GCS returns "IsTruncated" @JacksonXmlProperty(localName = "IsTruncated") @@ -60,6 +60,28 @@ public final class ListPartsResponse { @JacksonXmlProperty(localName = "Part") private List parts; + private ListPartsResponse(Builder builder) { + this.bucket = builder.bucket; + this.key = builder.key; + this.uploadId = builder.uploadId; + this.partNumberMarker = builder.partNumberMarker; + this.nextPartNumberMarker = builder.nextPartNumberMarker; + this.maxParts = builder.maxParts; + this.isTruncated = builder.isTruncated; + this.owner = builder.owner; + this.storageClass = builder.storageClass; + this.parts = builder.parts; + } + + /** + * Creates a new {@code Builder} for {@code ListPartsResponse} objects. + * + * @return A new {@code Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + /** * Returns the bucket name. * @@ -92,7 +114,7 @@ public String getUploadId() { * * @return the part number marker. */ - public Integer getPartNumberMarker() { + public int getPartNumberMarker() { return partNumberMarker; } @@ -101,7 +123,7 @@ public Integer getPartNumberMarker() { * * @return the next part number marker. */ - public Integer getNextPartNumberMarker() { + public int getNextPartNumberMarker() { return nextPartNumberMarker; } @@ -110,7 +132,7 @@ public Integer getNextPartNumberMarker() { * * @return the maximum number of parts to return. */ - public Integer getMaxParts() { + public int getMaxParts() { return maxParts; } @@ -201,4 +223,139 @@ public String toString() { .add("parts", parts) .toString(); } + + /** Builder for {@code ListPartsResponse}. */ + public static final class Builder { + private String bucket; + private String key; + private String uploadId; + private int partNumberMarker; + private int nextPartNumberMarker; + private int maxParts; + private boolean isTruncated; + private String owner; + private String storageClass; + private List parts; + + private Builder() {} + + /** + * Sets the bucket name. + * + * @param bucket The bucket name. + * @return The builder instance. + */ + public Builder setBucket(String bucket) { + this.bucket = bucket; + return this; + } + + /** + * Sets the object name. + * + * @param key The object name. + * @return The builder instance. + */ + public Builder setKey(String key) { + this.key = key; + return this; + } + + /** + * Sets the upload ID. + * + * @param uploadId The upload ID. + * @return The builder instance. + */ + public Builder setUploadId(String uploadId) { + this.uploadId = uploadId; + return this; + } + + /** + * Sets the part number marker. + * + * @param partNumberMarker The part number marker. + * @return The builder instance. + */ + public Builder setPartNumberMarker(int partNumberMarker) { + this.partNumberMarker = partNumberMarker; + return this; + } + + /** + * Sets the next part number marker. + * + * @param nextPartNumberMarker The next part number marker. + * @return The builder instance. + */ + public Builder setNextPartNumberMarker(int nextPartNumberMarker) { + this.nextPartNumberMarker = nextPartNumberMarker; + return this; + } + + /** + * Sets the maximum number of parts to return. + * + * @param maxParts The maximum number of parts to return. + * @return The builder instance. + */ + public Builder setMaxParts(int maxParts) { + this.maxParts = maxParts; + return this; + } + + /** + * Sets whether the response is truncated. + * + * @param isTruncated True if the response is truncated, false otherwise. + * @return The builder instance. + */ + public Builder setIsTruncated(boolean isTruncated) { + this.isTruncated = isTruncated; + return this; + } + + /** + * Sets the owner of the object. + * + * @param owner The owner of the object. + * @return The builder instance. + */ + public Builder setOwner(String owner) { + this.owner = owner; + return this; + } + + /** + * Sets the storage class of the object. + * + * @param storageClass The storage class of the object. + * @return The builder instance. + */ + public Builder setStorageClass(String storageClass) { + this.storageClass = storageClass; + return this; + } + + /** + * Sets the list of parts. + * + * @param parts The list of parts. + * @return The builder instance. + */ + public Builder setParts(List parts) { + this.parts = parts; + return this; + } + + /** + * Builds a {@code ListPartsResponse} object. + * + * @return A new {@code ListPartsResponse} instance. + */ + public ListPartsResponse build() { + return new ListPartsResponse(this); + } + } } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 182edb2da1..cf5ab5de45 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -414,8 +414,7 @@ public void sendListPartsRequest_success() throws Exception { .build(); ListPartsResponse response = - multipartUploadHttpRequestManager.sendListPartsRequest( - endpoint, request); + multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request); assertThat(response).isNotNull(); assertThat(response.getBucket()).isEqualTo("test-bucket"); @@ -455,9 +454,7 @@ public void sendListPartsRequest_error() throws Exception { assertThrows( HttpResponseException.class, - () -> - multipartUploadHttpRequestManager.sendListPartsRequest( - endpoint, request)); + () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request)); } } } From b261690fb018fdcf7762b549692ec17365608f55 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Sun, 26 Oct 2025 07:26:38 +0000 Subject: [PATCH 29/34] chore: fixed datatypes of few variables --- .../cloud/storage/MultipartUploadClient.java | 6 +-- .../storage/MultipartUploadClientImpl.java | 9 ++-- .../model/CreateMultipartUploadResponse.java | 8 ++-- .../model/ListPartsRequest.java | 1 + .../model/ListPartsResponse.java | 19 ++++---- .../storage/multipartupload/model/Part.java | 9 ++-- ...MultipartUploadHttpRequestManagerTest.java | 43 +++++++++++-------- pom.xml | 5 +++ 8 files changed, 60 insertions(+), 40 deletions(-) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java index 332becd5ca..2c6bd21c59 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java @@ -55,10 +55,9 @@ public abstract CreateMultipartUploadResponse createMultipartUpload( * * @param listPartsRequest The request object containing the details for listing the parts. * @return A {@link ListPartsResponse} object containing the list of parts. - * @throws IOException if an I/O error occurs. */ @BetaApi - public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest) throws IOException; + public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest); /** * Creates a new instance of {@link MultipartUploadClient}. @@ -72,6 +71,7 @@ public static MultipartUploadClient create(MultipartUploadSettings config) { return new MultipartUploadClientImpl( URI.create(options.getHost()), options.createRetrier(), - MultipartUploadHttpRequestManager.createFrom(options)); + MultipartUploadHttpRequestManager.createFrom(options), + options.getRetryAlgorithmManager()); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java index ae09fb2b15..664126a416 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java @@ -35,14 +35,17 @@ final class MultipartUploadClientImpl extends MultipartUploadClient { private final MultipartUploadHttpRequestManager httpRequestManager; private final Retrier retrier; private final URI uri; + private final HttpRetryAlgorithmManager retryAlgorithmManager; MultipartUploadClientImpl( URI uri, Retrier retrier, - MultipartUploadHttpRequestManager multipartUploadHttpRequestManager) { + MultipartUploadHttpRequestManager multipartUploadHttpRequestManager, + HttpRetryAlgorithmManager retryAlgorithmManager) { this.httpRequestManager = multipartUploadHttpRequestManager; this.retrier = retrier; this.uri = uri; + this.retryAlgorithmManager = retryAlgorithmManager; } @Override @@ -54,10 +57,10 @@ public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUpload @Override @BetaApi - public ListPartsResponse listParts(ListPartsRequest request) throws IOException { + public ListPartsResponse listParts(ListPartsRequest request) { return retrier.run( - Retrying.alwaysRetry(), + retryAlgorithmManager.idempotent(), () -> httpRequestManager.sendListPartsRequest(uri, request), Decoder.identity()); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java index d5c933de9f..f9a003ce67 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadResponse.java @@ -31,13 +31,15 @@ public final class CreateMultipartUploadResponse { @JacksonXmlProperty(localName = "Bucket") - private final String bucket; + private String bucket; @JacksonXmlProperty(localName = "Key") - private final String key; + private String key; @JacksonXmlProperty(localName = "UploadId") - private final String uploadId; + private String uploadId; + + private CreateMultipartUploadResponse() {} private CreateMultipartUploadResponse(Builder builder) { this.bucket = builder.bucket; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java index 5c3dcedf76..51d23daf16 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java @@ -128,6 +128,7 @@ public static Builder builder() { } /** A builder for {@link ListPartsRequest}. */ + @BetaApi public static class Builder { private String bucket; private String key; diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 6be2f186c4..05984fe05f 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.google.api.core.BetaApi; +import com.google.cloud.storage.Acl; +import com.google.cloud.storage.StorageClass; import com.google.common.base.MoreObjects; import java.util.List; import java.util.Objects; @@ -51,10 +53,10 @@ public final class ListPartsResponse { private boolean isTruncated; @JacksonXmlProperty(localName = "Owner") - private String owner; + private Acl.Entity owner; @JacksonXmlProperty(localName = "StorageClass") - private String storageClass; + private StorageClass storageClass; @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "Part") @@ -150,7 +152,7 @@ public boolean isTruncated() { * * @return the owner of the object. */ - public String getOwner() { + public Acl.Entity getOwner() { return owner; } @@ -159,7 +161,7 @@ public String getOwner() { * * @return the storage class of the object. */ - public String getStorageClass() { + public StorageClass getStorageClass() { return storageClass; } @@ -225,6 +227,7 @@ public String toString() { } /** Builder for {@code ListPartsResponse}. */ + @BetaApi public static final class Builder { private String bucket; private String key; @@ -233,8 +236,8 @@ public static final class Builder { private int nextPartNumberMarker; private int maxParts; private boolean isTruncated; - private String owner; - private String storageClass; + private Acl.Entity owner; + private StorageClass storageClass; private List parts; private Builder() {} @@ -322,7 +325,7 @@ public Builder setIsTruncated(boolean isTruncated) { * @param owner The owner of the object. * @return The builder instance. */ - public Builder setOwner(String owner) { + public Builder setOwner(Acl.Entity owner) { this.owner = owner; return this; } @@ -333,7 +336,7 @@ public Builder setOwner(String owner) { * @param storageClass The storage class of the object. * @return The builder instance. */ - public Builder setStorageClass(String storageClass) { + public Builder setStorageClass(StorageClass storageClass) { this.storageClass = storageClass; return this; } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java index 47599b2380..1360b24da1 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.google.common.base.MoreObjects; +import java.time.OffsetDateTime; import java.util.Objects; /** Represents a part of a multipart upload. */ @@ -33,7 +34,7 @@ public final class Part { private long size; @JacksonXmlProperty(localName = "LastModified") - private String lastModified; + private OffsetDateTime lastModified; // for jackson private Part() {} @@ -77,7 +78,7 @@ public long size() { * * @return the last modified time of the part. */ - public String lastModified() { + public OffsetDateTime lastModified() { return lastModified; } @@ -125,7 +126,7 @@ public static final class Builder { private int partNumber; private String eTag; private long size; - private String lastModified; + private OffsetDateTime lastModified; private Builder() {} @@ -168,7 +169,7 @@ public Builder size(long size) { * @param lastModified the last modified time of the part. * @return this builder. */ - public Builder lastModified(String lastModified) { + public Builder lastModified(OffsetDateTime lastModified) { this.lastModified = lastModified; return this; } diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index cf5ab5de45..6f77e427d6 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -44,6 +44,7 @@ import java.net.URI; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.Collections; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -55,6 +56,7 @@ @ParallelFriendly public final class ITMultipartUploadHttpRequestManagerTest { private static final XmlMapper xmlMapper = new XmlMapper(); + private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager; @Rule public final TemporaryFolder temp = new TemporaryFolder(); @@ -377,24 +379,26 @@ public void sendCreateMultipartUploadRequest_withCustomTime() throws Exception { public void sendListPartsRequest_success() throws Exception { HttpRequestHandler handler = req -> { - String xmlResponse = - "\n" - + "\n" - + " test-bucket\n" - + " test-key\n" - + " test-upload-id\n" - + " 0\n" - + " 1\n" - + " 1\n" - + " false\n" - + " \n" - + " 1\n" - + " \"etag\"\n" - + " 123\n" - + " 2024-05-08T17:50:00.000Z\n" - + " \n" - + ""; - ByteBuf buf = Unpooled.wrappedBuffer(xmlResponse.getBytes()); + OffsetDateTime lastModified = OffsetDateTime.of(2024, 5, 8, 17, 50, 0, 0, ZoneOffset.UTC); + ListPartsResponse listPartsResponse = + ListPartsResponse.builder() + .setBucket("test-bucket") + .setKey("test-key") + .setUploadId("test-upload-id") + .setPartNumberMarker(0) + .setNextPartNumberMarker(1) + .setMaxParts(1) + .setIsTruncated(false) + .setParts( + Collections.singletonList( + Part.builder() + .partNumber(1) + .eTag("\"etag\"") + .size(123) + .lastModified(lastModified) + .build())) + .build(); + ByteBuf buf = Unpooled.wrappedBuffer(xmlMapper.writeValueAsBytes(listPartsResponse)); DefaultFullHttpResponse resp = new DefaultFullHttpResponse(req.protocolVersion(), OK, buf); @@ -429,7 +433,8 @@ public void sendListPartsRequest_success() throws Exception { assertThat(part.partNumber()).isEqualTo(1); assertThat(part.eTag()).isEqualTo("\"etag\""); assertThat(part.size()).isEqualTo(123); - assertThat(part.lastModified()).isEqualTo("2024-05-08T17:50:00.000Z"); + assertThat(part.lastModified()) + .isEqualTo(OffsetDateTime.of(2024, 5, 8, 17, 50, 0, 0, ZoneOffset.UTC)); } } diff --git a/pom.xml b/pom.xml index fa0d915d37..09ce9c2afe 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,11 @@ pom import + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.13.4 + org.junit junit-bom From 6acb2a27a0bd1890f64f05b7b29f380f69a2ea0b Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Sun, 26 Oct 2025 08:02:38 +0000 Subject: [PATCH 30/34] chore:adding handling for storage class nad owner --- google-cloud-storage/pom.xml | 4 + .../google/cloud/storage/StorageClass.java | 5 + .../google/cloud/storage/XmlObjectParser.java | 2 + .../model/ListPartsResponse.java | 11 +- .../storage/multipartupload/model/Owner.java | 130 ++++++++++++++++++ ...MultipartUploadHttpRequestManagerTest.java | 8 +- 6 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 855a1f999a..8392982726 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -23,6 +23,10 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + com.google.guava guava diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java index 07efcbf842..1986b524c4 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClass.java @@ -15,6 +15,7 @@ */ package com.google.cloud.storage; +import com.fasterxml.jackson.annotation.JsonCreator; import com.google.api.core.ApiFunction; import com.google.cloud.StringEnumType; import com.google.cloud.StringEnumValue; @@ -111,7 +112,11 @@ public static StorageClass valueOfStrict(String constant) { } /** Get the StorageClass for the given String constant, and allow unrecognized values. */ + @JsonCreator public static StorageClass valueOf(String constant) { + if (constant == null || constant.isEmpty()) { + return null; + } return type.valueOf(constant); } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java index 27c91fef48..7b56832c76 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/XmlObjectParser.java @@ -16,6 +16,7 @@ package com.google.cloud.storage; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.api.client.util.ObjectParser; import com.google.common.annotations.VisibleForTesting; import java.io.IOException; @@ -31,6 +32,7 @@ final class XmlObjectParser implements ObjectParser { @VisibleForTesting public XmlObjectParser(XmlMapper xmlMapper) { this.xmlMapper = xmlMapper; + this.xmlMapper.registerModule(new JavaTimeModule()); } @Override diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 05984fe05f..842607baaa 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.google.api.core.BetaApi; -import com.google.cloud.storage.Acl; import com.google.cloud.storage.StorageClass; import com.google.common.base.MoreObjects; import java.util.List; @@ -53,7 +52,7 @@ public final class ListPartsResponse { private boolean isTruncated; @JacksonXmlProperty(localName = "Owner") - private Acl.Entity owner; + private Owner owner; @JacksonXmlProperty(localName = "StorageClass") private StorageClass storageClass; @@ -62,6 +61,8 @@ public final class ListPartsResponse { @JacksonXmlProperty(localName = "Part") private List parts; + private ListPartsResponse() {} + private ListPartsResponse(Builder builder) { this.bucket = builder.bucket; this.key = builder.key; @@ -152,7 +153,7 @@ public boolean isTruncated() { * * @return the owner of the object. */ - public Acl.Entity getOwner() { + public Owner getOwner() { return owner; } @@ -236,7 +237,7 @@ public static final class Builder { private int nextPartNumberMarker; private int maxParts; private boolean isTruncated; - private Acl.Entity owner; + private Owner owner; private StorageClass storageClass; private List parts; @@ -325,7 +326,7 @@ public Builder setIsTruncated(boolean isTruncated) { * @param owner The owner of the object. * @return The builder instance. */ - public Builder setOwner(Acl.Entity owner) { + public Builder setOwner(Owner owner) { this.owner = owner; return this; } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java new file mode 100644 index 0000000000..9ec07f5c63 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java @@ -0,0 +1,130 @@ +/* + * Copyright 2025 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.multipartupload.model; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.google.common.base.MoreObjects; +import java.util.Objects; + +/** Represents the owner of a resource. */ +public final class Owner { + + @JacksonXmlProperty(localName = "ID") + private String id; + + @JacksonXmlProperty(localName = "DisplayName") + private String displayName; + + // for jackson + private Owner() {} + + private Owner(Builder builder) { + this.id = builder.id; + this.displayName = builder.displayName; + } + + /** + * Returns the ID of the owner. + * + * @return the ID of the owner. + */ + public String getId() { + return id; + } + + /** + * Returns the display name of the owner. + * + * @return the display name of the owner. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Returns a new builder for this class. + * + * @return a new builder for this class. + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Owner)) { + return false; + } + Owner owner = (Owner) o; + return Objects.equals(id, owner.id) && Objects.equals(displayName, owner.displayName); + } + + @Override + public int hashCode() { + return Objects.hash(id, displayName); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("displayName", displayName) + .toString(); + } + + /** A builder for {@link Owner}. */ + public static final class Builder { + private String id; + private String displayName; + + private Builder() {} + + /** + * Sets the ID of the owner. + * + * @param id the ID of the owner. + * @return this builder. + */ + public Builder setId(String id) { + this.id = id; + return this; + } + + /** + * Sets the display name of the owner. + * + * @param displayName the display name of the owner. + * @return this builder. + */ + public Builder setDisplayName(String displayName) { + this.displayName = displayName; + return this; + } + + /** + * Builds a new {@link Owner} object. + * + * @return a new {@link Owner} object. + */ + public Owner build() { + return new Owner(this); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 6f77e427d6..0d2b80cb05 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertThrows; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.api.client.http.HttpResponseException; import com.google.cloud.NoCredentials; import com.google.cloud.storage.FakeHttpServer.HttpRequestHandler; @@ -55,7 +56,12 @@ @SingleBackend(Backend.PROD) @ParallelFriendly public final class ITMultipartUploadHttpRequestManagerTest { - private static final XmlMapper xmlMapper = new XmlMapper(); + private static final XmlMapper xmlMapper; + + static { + xmlMapper = new XmlMapper(); + xmlMapper.registerModule(new JavaTimeModule()); + } private MultipartUploadHttpRequestManager multipartUploadHttpRequestManager; @Rule public final TemporaryFolder temp = new TemporaryFolder(); From ddfa9fd816c0c826a5e8053e1b168b29462cf00b Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Sun, 26 Oct 2025 08:18:05 +0000 Subject: [PATCH 31/34] chore: Removed unwanted library decleration --- pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pom.xml b/pom.xml index 09ce9c2afe..fa0d915d37 100644 --- a/pom.xml +++ b/pom.xml @@ -66,11 +66,6 @@ pom import - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.13.4 - org.junit junit-bom From 337f9721b9e6d2e6992a9d48db5186c16d250cb0 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Sun, 26 Oct 2025 10:20:08 +0000 Subject: [PATCH 32/34] chore: Adding seperate test for all use case of List parts. --- ...MultipartUploadHttpRequestManagerTest.java | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java index 0d2b80cb05..8d3455a4e3 100644 --- a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITMultipartUploadHttpRequestManagerTest.java @@ -43,6 +43,7 @@ import io.grpc.netty.shaded.io.netty.handler.codec.http.FullHttpResponse; import io.grpc.netty.shaded.io.netty.handler.codec.http.HttpResponseStatus; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Collections; @@ -445,7 +446,79 @@ public void sendListPartsRequest_success() throws Exception { } @Test - public void sendListPartsRequest_error() throws Exception { + public void sendListPartsRequest_bucketNotFound() throws Exception { + HttpRequestHandler handler = + req -> + new DefaultFullHttpResponse( + req.protocolVersion(), + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("Bucket not found".getBytes(StandardCharsets.UTF_8))); + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + ListPartsRequest request = + ListPartsRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + + assertThrows( + HttpResponseException.class, + () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request)); + } + } + + @Test + public void sendListPartsRequest_keyNotFound() throws Exception { + HttpRequestHandler handler = + req -> + new DefaultFullHttpResponse( + req.protocolVersion(), + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("Key not found".getBytes(StandardCharsets.UTF_8))); + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + ListPartsRequest request = + ListPartsRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("test-upload-id") + .build(); + + assertThrows( + HttpResponseException.class, + () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request)); + } + } + + @Test + public void sendListPartsRequest_badRequest() throws Exception { + HttpRequestHandler handler = + req -> + new DefaultFullHttpResponse( + req.protocolVersion(), + HttpResponseStatus.BAD_REQUEST, + Unpooled.wrappedBuffer("Invalid uploadId".getBytes(StandardCharsets.UTF_8))); + + try (FakeHttpServer fakeHttpServer = FakeHttpServer.of(handler)) { + URI endpoint = fakeHttpServer.getEndpoint(); + ListPartsRequest request = + ListPartsRequest.builder() + .bucket("test-bucket") + .key("test-key") + .uploadId("invalid-upload-id") + .build(); + + assertThrows( + HttpResponseException.class, + () -> multipartUploadHttpRequestManager.sendListPartsRequest(endpoint, request)); + } + } + + @Test + public void sendListPartsRequest_errorResponse() throws Exception { HttpRequestHandler handler = req -> { FullHttpResponse resp = From c7549b10d56e40c5576a6a7f471d7cd6f8bba397 Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Tue, 28 Oct 2025 19:00:03 +0000 Subject: [PATCH 33/34] chore: adding beta api annotation. --- .../com/google/cloud/storage/multipartupload/model/Part.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java index 1360b24da1..61e639b823 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Part.java @@ -17,6 +17,7 @@ package com.google.cloud.storage.multipartupload.model; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.google.api.core.BetaApi; import com.google.common.base.MoreObjects; import java.time.OffsetDateTime; import java.util.Objects; @@ -122,6 +123,7 @@ public String toString() { } /** A builder for {@link Part}. */ + @BetaApi public static final class Builder { private int partNumber; private String eTag; From 3869259f9e89131238b98ee4a4ed861b1e1c971c Mon Sep 17 00:00:00 2001 From: Shreyas Sinha Date: Thu, 30 Oct 2025 18:43:30 +0000 Subject: [PATCH 34/34] chore: added review comments --- google-cloud-storage/pom.xml | 8 ++ .../model/ListPartsResponse.java | 28 ---- .../storage/multipartupload/model/Owner.java | 130 ------------------ 3 files changed, 8 insertions(+), 158 deletions(-) delete mode 100644 google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 8392982726..b495f8665e 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -27,6 +27,14 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + com.google.guava guava diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java index 842607baaa..2311190ff3 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java @@ -51,9 +51,6 @@ public final class ListPartsResponse { @JacksonXmlProperty(localName = "IsTruncated") private boolean isTruncated; - @JacksonXmlProperty(localName = "Owner") - private Owner owner; - @JacksonXmlProperty(localName = "StorageClass") private StorageClass storageClass; @@ -71,7 +68,6 @@ private ListPartsResponse(Builder builder) { this.nextPartNumberMarker = builder.nextPartNumberMarker; this.maxParts = builder.maxParts; this.isTruncated = builder.isTruncated; - this.owner = builder.owner; this.storageClass = builder.storageClass; this.parts = builder.parts; } @@ -148,15 +144,6 @@ public boolean isTruncated() { return isTruncated; } - /** - * Returns the owner of the object. - * - * @return the owner of the object. - */ - public Owner getOwner() { - return owner; - } - /** * Returns the storage class of the object. * @@ -191,7 +178,6 @@ public boolean equals(Object o) { && Objects.equals(nextPartNumberMarker, that.nextPartNumberMarker) && Objects.equals(maxParts, that.maxParts) && Objects.equals(isTruncated, that.isTruncated) - && Objects.equals(owner, that.owner) && Objects.equals(storageClass, that.storageClass) && Objects.equals(parts, that.parts); } @@ -206,7 +192,6 @@ public int hashCode() { nextPartNumberMarker, maxParts, isTruncated, - owner, storageClass, parts); } @@ -221,7 +206,6 @@ public String toString() { .add("nextPartNumberMarker", nextPartNumberMarker) .add("maxParts", maxParts) .add("isTruncated", isTruncated) - .add("owner", owner) .add("storageClass", storageClass) .add("parts", parts) .toString(); @@ -237,7 +221,6 @@ public static final class Builder { private int nextPartNumberMarker; private int maxParts; private boolean isTruncated; - private Owner owner; private StorageClass storageClass; private List parts; @@ -320,17 +303,6 @@ public Builder setIsTruncated(boolean isTruncated) { return this; } - /** - * Sets the owner of the object. - * - * @param owner The owner of the object. - * @return The builder instance. - */ - public Builder setOwner(Owner owner) { - this.owner = owner; - return this; - } - /** * Sets the storage class of the object. * diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java deleted file mode 100644 index 9ec07f5c63..0000000000 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/Owner.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2025 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.multipartupload.model; - -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.google.common.base.MoreObjects; -import java.util.Objects; - -/** Represents the owner of a resource. */ -public final class Owner { - - @JacksonXmlProperty(localName = "ID") - private String id; - - @JacksonXmlProperty(localName = "DisplayName") - private String displayName; - - // for jackson - private Owner() {} - - private Owner(Builder builder) { - this.id = builder.id; - this.displayName = builder.displayName; - } - - /** - * Returns the ID of the owner. - * - * @return the ID of the owner. - */ - public String getId() { - return id; - } - - /** - * Returns the display name of the owner. - * - * @return the display name of the owner. - */ - public String getDisplayName() { - return displayName; - } - - /** - * Returns a new builder for this class. - * - * @return a new builder for this class. - */ - public static Builder builder() { - return new Builder(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Owner)) { - return false; - } - Owner owner = (Owner) o; - return Objects.equals(id, owner.id) && Objects.equals(displayName, owner.displayName); - } - - @Override - public int hashCode() { - return Objects.hash(id, displayName); - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("id", id) - .add("displayName", displayName) - .toString(); - } - - /** A builder for {@link Owner}. */ - public static final class Builder { - private String id; - private String displayName; - - private Builder() {} - - /** - * Sets the ID of the owner. - * - * @param id the ID of the owner. - * @return this builder. - */ - public Builder setId(String id) { - this.id = id; - return this; - } - - /** - * Sets the display name of the owner. - * - * @param displayName the display name of the owner. - * @return this builder. - */ - public Builder setDisplayName(String displayName) { - this.displayName = displayName; - return this; - } - - /** - * Builds a new {@link Owner} object. - * - * @return a new {@link Owner} object. - */ - public Owner build() { - return new Owner(this); - } - } -}