diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 222d2cf555710..76b666e436e9e 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_018b1a065c" + "Tag": "java/storage/azure-storage-blob_e4e816b0d8" } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java index 6f3a12dd90a5a..1caeec6b80fa6 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java @@ -1122,33 +1122,17 @@ PagedFlux listBlobsFlatWithOptionalTimeout(ListBlobsOptions options, S Duration timeout) { ModelHelper.validateListBlobsOptions(options); BiFunction>> func = (marker, pageSize) -> { - ListBlobsOptions finalOptions; + ListBlobsOptions finalOptions = ModelHelper.copyListBlobsOptions(options); + ModelHelper.applyDefaultsToListBlobsOptions(finalOptions); /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. Otherwise, prefer the new value. - */ + */ if (pageSize != null) { - if (options == null) { - finalOptions = new ListBlobsOptions().setMaxResultsPerPage(pageSize); - } else { - // Note that this prefers the value passed to .byPage(int) over the value on the options - finalOptions = new ListBlobsOptions().setMaxResultsPerPage(pageSize) - .setPrefix(options.getPrefix()) - .setDetails(options.getDetails()) - .setStartFrom(options.getStartFrom()); - if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { - finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) - .setEndBefore(options.getEndBefore()); - } - } - } else { - finalOptions = options; + finalOptions.setMaxResultsPerPage(pageSize); } - if (finalOptions != null - && ModelHelper.resolveSerializationFormat(finalOptions.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { + if (finalOptions.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { return listBlobsFlatSegmentArrow(marker, finalOptions, timeout); } @@ -1366,33 +1350,17 @@ PagedFlux listBlobsHierarchyWithOptionalTimeout(String delimiter, List Duration timeout) { ModelHelper.validateListBlobsOptions(options); BiFunction>> func = (marker, pageSize) -> { - ListBlobsOptions finalOptions; + ListBlobsOptions finalOptions = ModelHelper.copyListBlobsOptions(options); + ModelHelper.applyDefaultsToListBlobsOptions(finalOptions); /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. Otherwise, prefer the new value. - */ + */ if (pageSize != null) { - if (options == null) { - finalOptions = new ListBlobsOptions().setMaxResultsPerPage(pageSize); - } else { - // Note that this prefers the value passed to .byPage(int) over the value on the options - finalOptions = new ListBlobsOptions().setMaxResultsPerPage(pageSize) - .setPrefix(options.getPrefix()) - .setDetails(options.getDetails()) - .setStartFrom(options.getStartFrom()); - if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { - finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) - .setEndBefore(options.getEndBefore()); - } - } - } else { - finalOptions = options; + finalOptions.setMaxResultsPerPage(pageSize); } - if (finalOptions != null - && ModelHelper.resolveSerializationFormat(finalOptions.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { + if (finalOptions.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { return listBlobsHierarchySegmentArrow(marker, delimiter, finalOptions, timeout); } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java index 67dc02480be42..4700c6f22dcde 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java @@ -1035,19 +1035,8 @@ public PagedIterable listBlobs(ListBlobsOptions options, Duration time public PagedIterable listBlobs(ListBlobsOptions options, String continuationToken, Duration timeout) { ModelHelper.validateListBlobsOptions(options); BiFunction> retriever = (nextMarker, pageSize) -> { - ListBlobsOptions finalOptions = new ListBlobsOptions(); - if (options != null) { - finalOptions.setMaxResultsPerPage(options.getMaxResultsPerPage()) - .setPrefix(options.getPrefix()) - .setStartFrom(options.getStartFrom()) - .setDetails(options.getDetails()); - - if (options.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { - finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) - .setEndBefore(options.getEndBefore()); - } - - } + ListBlobsOptions finalOptions = ModelHelper.copyListBlobsOptions(options); + ModelHelper.applyDefaultsToListBlobsOptions(finalOptions); /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. Otherwise, prefer the new value. @@ -1230,18 +1219,8 @@ public PagedIterable listBlobsByHierarchy(String directory) { public PagedIterable listBlobsByHierarchy(String delimiter, ListBlobsOptions options, Duration timeout) { ModelHelper.validateListBlobsOptions(options); BiFunction> func = (marker, pageSize) -> { - ListBlobsOptions finalOptions = new ListBlobsOptions(); - if (options != null) { - finalOptions.setMaxResultsPerPage(options.getMaxResultsPerPage()) - .setPrefix(options.getPrefix()) - .setDetails(options.getDetails()) - .setStartFrom(options.getStartFrom()); - if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { - finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) - .setEndBefore(options.getEndBefore()); - } - } + ListBlobsOptions finalOptions = ModelHelper.copyListBlobsOptions(options); + ModelHelper.applyDefaultsToListBlobsOptions(finalOptions); /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. Otherwise, prefer the new value. @@ -1263,8 +1242,7 @@ private PagedResponse listBlobsHierarchySegment(String marker, String ArrayList include = options.getDetails().toList().isEmpty() ? null : options.getDetails().toList(); - if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) - == StorageResponseSerializationFormat.ARROW) { + if (options.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { Callable> operation = () -> azureBlobStorage.getContainers() .listBlobHierarchySegmentApacheArrowWithResponse(containerName, delimiter, options.getPrefix(), diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java index e5c79d643864f..28243d87611d4 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java @@ -93,7 +93,7 @@ public final class ModelHelper { * but it is not a public API change. */ private static final StorageResponseSerializationFormat DEFAULT_SERIALIZATION_FORMAT - = StorageResponseSerializationFormat.XML; + = StorageResponseSerializationFormat.ARROW; /** * Determines whether the passed authority is IP style, that is, it is of the format {@code :}. @@ -689,6 +689,34 @@ public static BlobStorageException mapToBlobStorageException(BlobStorageExceptio return format; } + /** + * Copies list blobs options. + * + * @param options the listing options to copy, or {@code null}. + * @return the copied options. + */ + public static ListBlobsOptions copyListBlobsOptions(ListBlobsOptions options) { + if (options == null) { + return new ListBlobsOptions(); + } + return new ListBlobsOptions().setMaxResultsPerPage(options.getMaxResultsPerPage()) + .setPrefix(options.getPrefix()) + .setStartFrom(options.getStartFrom()) + .setEndBefore(options.getEndBefore()) + .setDetails(options.getDetails()) + .setStorageResponseSerializationFormat(options.getStorageResponseSerializationFormat()); + } + + /** + * Replaces the requested serialization format with the concrete wire format. + * + * @param options the listing options to update. + */ + public static void applyDefaultsToListBlobsOptions(ListBlobsOptions options) { + options.setStorageResponseSerializationFormat( + resolveSerializationFormat(options.getStorageResponseSerializationFormat())); + } + /** * Validates that {@code endBefore}, when specified, is used with the Apache Arrow response format. * diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerAsyncClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerAsyncClientTests.java new file mode 100644 index 0000000000000..770c5950fb4df --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerAsyncClientTests.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.test.utils.MockTokenCredential; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; +import com.azure.storage.common.implementation.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BlobContainerAsyncClientTests { + private AtomicReference acceptHeader; + private BlobContainerAsyncClient client; + + @BeforeEach + public void setup() { + acceptHeader = new AtomicReference<>(); + HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(request -> { + acceptHeader.set(request.getHeaders().getValue(HttpHeaderName.ACCEPT)); + // Fail after capturing the request so the test doesn't need a format-specific response body. + return Mono.just(new MockHttpResponse(request, 500)); + }).build(); + client = new BlobContainerClientBuilder().endpoint("https://account.blob.core.windows.net/container") + .credential(new MockTokenCredential()) + .pipeline(pipeline) + .buildAsyncClient(); + } + + @ParameterizedTest + @MethodSource("serializationFormatSupplier") + public void listBlobsUsesResolvedSerializationFormat(ListBlobsOptions options, String expectedAcceptHeader) { + sendRequest(() -> client.listBlobs(options).byPage(1).blockFirst()); + assertEquals(expectedAcceptHeader, acceptHeader.get()); + } + + @ParameterizedTest + @MethodSource("serializationFormatSupplier") + public void listBlobsByHierarchyUsesResolvedSerializationFormat(ListBlobsOptions options, + String expectedAcceptHeader) { + sendRequest(() -> client.listBlobsByHierarchy("/", options).byPage(1).blockFirst()); + assertEquals(expectedAcceptHeader, acceptHeader.get()); + } + + private static void sendRequest(Supplier request) { + try { + request.get(); + } catch (BlobStorageException exception) { + if (exception.getStatusCode() != 500) { + throw exception; + } + } + } + + private static Stream serializationFormatSupplier() { + String arrowAcceptHeader + = Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM + ",application/xml"; + return Stream.of(Arguments.of(null, arrowAcceptHeader), Arguments.of(new ListBlobsOptions(), arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.AUTO), + arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW), + arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.XML), + "application/xml")); + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerClientTests.java new file mode 100644 index 0000000000000..4860813bb131c --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobContainerClientTests.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.test.utils.MockTokenCredential; +import com.azure.storage.blob.models.BlobStorageException; +import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; +import com.azure.storage.common.implementation.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BlobContainerClientTests { + private AtomicReference acceptHeader; + private BlobContainerClient client; + + @BeforeEach + public void setup() { + acceptHeader = new AtomicReference<>(); + HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(request -> { + acceptHeader.set(request.getHeaders().getValue(HttpHeaderName.ACCEPT)); + // Fail after capturing the request so the test doesn't need a format-specific response body. + return Mono.just(new MockHttpResponse(request, 500)); + }).build(); + client = new BlobContainerClientBuilder().endpoint("https://account.blob.core.windows.net/container") + .credential(new MockTokenCredential()) + .pipeline(pipeline) + .buildClient(); + } + + @ParameterizedTest + @MethodSource("serializationFormatSupplier") + public void listBlobsUsesResolvedSerializationFormat(ListBlobsOptions options, String expectedAcceptHeader) { + sendRequest(() -> client.listBlobs(options, null).iterableByPage(1).iterator().hasNext()); + assertEquals(expectedAcceptHeader, acceptHeader.get()); + } + + @ParameterizedTest + @MethodSource("serializationFormatSupplier") + public void listBlobsByHierarchyUsesResolvedSerializationFormat(ListBlobsOptions options, + String expectedAcceptHeader) { + sendRequest(() -> client.listBlobsByHierarchy("/", options, null).iterableByPage(1).iterator().hasNext()); + assertEquals(expectedAcceptHeader, acceptHeader.get()); + } + + private static void sendRequest(Supplier request) { + try { + request.get(); + } catch (BlobStorageException exception) { + if (exception.getStatusCode() != 500) { + throw exception; + } + } + } + + private static Stream serializationFormatSupplier() { + String arrowAcceptHeader + = Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM + ",application/xml"; + return Stream.of(Arguments.of(null, arrowAcceptHeader), Arguments.of(new ListBlobsOptions(), arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.AUTO), + arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW), + arrowAcceptHeader), + Arguments.of( + new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.XML), + "application/xml")); + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobErrorDeserializationTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobErrorDeserializationTests.java deleted file mode 100644 index a9561ee4882fc..0000000000000 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobErrorDeserializationTests.java +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -package com.azure.storage.blob; - -import com.azure.core.http.HttpHeaderName; -import com.azure.core.http.HttpHeaders; -import com.azure.core.http.HttpPipeline; -import com.azure.core.http.HttpPipelineBuilder; -import com.azure.core.test.http.MockHttpResponse; -import com.azure.core.test.utils.MockTokenCredential; -import com.azure.storage.blob.models.BlobStorageException; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import java.nio.charset.StandardCharsets; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Tests cases where the service returns a response that would result in a {@link BlobStorageException} being thrown - * with a response body that needs to be deserialized. - */ -public class BlobErrorDeserializationTests { - @Test - public void errorResponseBody() { - String errorResponse = "ContainerAlreadyExists" - + "The specified container already exists."; - HttpPipeline httpPipeline = new HttpPipelineBuilder().httpClient(request -> Mono.just( - new MockHttpResponse(request, 409, new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/xml"), - errorResponse.getBytes(StandardCharsets.UTF_8)))) - .build(); - BlobContainerClient containerClient - = new BlobContainerClientBuilder().endpoint("https://account.blob.core.windows.net/container") - .credential(new MockTokenCredential()) - .pipeline(httpPipeline) - .buildClient(); - - BlobStorageException exception = assertThrows(BlobStorageException.class, containerClient::create); - assertTrue(exception.getMessage().contains("The specified container already exists.")); - // assertEquals(BlobErrorCode.CONTAINER_ALREADY_EXISTS, exception.getErrorCode()); - } -} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java index 5bdf5cbd0c0e3..62cee0c83da78 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java @@ -2279,8 +2279,7 @@ public void listBlobsArrowPagination() { @Test @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") - public void listBlobsArrowNullUseArrowUsesXml() { - // Default apacheArrowEnabled is null — should use XML path without error + public void listBlobsArrowNullDefaultsToArrow() { String blobName = generateBlobName(); cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java index 568603acdb9ab..009e335012c2c 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java @@ -2250,8 +2250,7 @@ public void listBlobsArrowPagination() { @Test @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") - public void listBlobsArrowNullUseArrowUsesXml() { - // Default apacheArrowEnabled is null — should use XML path without error + public void listBlobsArrowNullDefaultsToArrow() { String blobName = generateBlobName(); BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();