diff --git a/NOTICE.txt b/NOTICE.txt index ef12f9a58bdc..391057bad80d 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -500,6 +500,23 @@ See the License for the specific language governing permissions and limitations ------------------------------------------------------------------------------------------------- +License notice for Apache Arrow +------------------------------------------------------------------------------- + +apache-arrow-java (https://github.com/apache/arrow-java) +Copyright 2015-2024 The Apache Software Foundation + +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. + +------------------------------------------------------------------------------------------------- + License notice for GraalVM ------------------------------------------------------------------------------ org.graalvm.sdk:graal-sdk - https://github.com/graalvm/native-build-tools/blob/master/common/junit-platform-native/LICENSE @@ -609,4 +626,4 @@ compliance with the License. You may obtain a copy of the License at: 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. \ No newline at end of file +language governing permissions and limitations under the License. diff --git a/eng/versioning/external_dependencies.txt b/eng/versioning/external_dependencies.txt index 824fcb55c273..e8b29ee9ea20 100644 --- a/eng/versioning/external_dependencies.txt +++ b/eng/versioning/external_dependencies.txt @@ -25,6 +25,7 @@ com.github.spotbugs:spotbugs;4.8.3 com.github.spotbugs:spotbugs-annotations;4.8.3 com.github.spotbugs:spotbugs-maven-plugin;4.8.3.1 com.google.code.gson:gson;2.14.0 +com.google.flatbuffers:flatbuffers-java;24.3.25 com.google.guava:guava;33.6.0-jre com.h2database:h2;2.2.220 com.h3xstream.findsecbugs:findsecbugs-plugin;1.9.0 @@ -63,6 +64,7 @@ io.vertx:vertx-codegen;4.5.27 io.vertx:vertx-core;4.5.27 javax.websocket:javax.websocket-api;1.1 org.apache.ant:ant;1.10.15 +org.apache.arrow:arrow-format;19.0.0 org.apache.avro:avro;1.11.4 org.apache.avro:avro-maven-plugin;1.11.4 org.apache.commons:commons-lang3;3.18.0 diff --git a/sdk/parents/azure-client-sdk-parent/pom.xml b/sdk/parents/azure-client-sdk-parent/pom.xml index ae363a524be1..2079dbfd3b53 100644 --- a/sdk/parents/azure-client-sdk-parent/pom.xml +++ b/sdk/parents/azure-client-sdk-parent/pom.xml @@ -602,6 +602,10 @@ com.google.code.findbugs:jsr305:[3.0.2] + + + com.google.flatbuffers:flatbuffers-java:[24.3.25] diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index b6f9de0cec63..d5cf9765e84f 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_802f7f8947" + "Tag": "java/storage/azure-storage-blob_13b4ad4e16" } diff --git a/sdk/storage/azure-storage-blob/pom.xml b/sdk/storage/azure-storage-blob/pom.xml index 4c591a7d5304..90796b484e71 100644 --- a/sdk/storage/azure-storage-blob/pom.xml +++ b/sdk/storage/azure-storage-blob/pom.xml @@ -59,6 +59,10 @@ concurrent + + **/implementation/util/apachearrow/**/*.java + **/implementation/util/apachearrow/**/*.java + false @@ -138,6 +142,20 @@ 1.17.7 test + + + + + com.google.flatbuffers + flatbuffers-java + 24.3.25 + + @@ -182,5 +200,84 @@ + + + + + + + + + shade-apache-arrow-format + + + shade-apache-arrow-format + + + + + + org.apache.arrow + arrow-format + 19.0.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + + shade + package + + shade + + + + + + true + true + true + shade-apache-arrow-format + false + + + + org.apache.arrow:arrow-format + + + + + + org.apache.arrow.flatbuf + com.azure.storage.blob.implementation.util.apachearrow + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.6.1 + + + + + org.apache.arrow:arrow-format:[19.0.0] + + + + + + + + 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 b86fe4e76b2f..44650de89a14 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 @@ -28,6 +28,8 @@ import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse; import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer.ArrowListBlobsResult; import com.azure.storage.blob.implementation.util.BlobConstants; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; import com.azure.storage.blob.implementation.util.ModelHelper; @@ -42,6 +44,7 @@ import com.azure.storage.blob.models.CustomerProvidedKey; import com.azure.storage.blob.models.ListBlobsIncludeItem; import com.azure.storage.blob.models.ListBlobsOptions; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.StorageAccountInfo; import com.azure.storage.blob.models.TaggedBlobItem; @@ -51,6 +54,7 @@ import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; import com.azure.storage.common.Utility; +import com.azure.storage.common.implementation.Constants; import com.azure.storage.common.implementation.SasImplUtils; import com.azure.storage.common.implementation.StorageImplUtils; import reactor.core.publisher.Mono; @@ -1118,18 +1122,35 @@ PagedFlux listBlobsFlatWithOptionalTimeout(ListBlobsOptions options, S Duration timeout) { BiFunction>> func = (marker, pageSize) -> { ListBlobsOptions 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()); + .setDetails(options.getDetails()) + .setStartFrom(options.getStartFrom()); + if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) + == StorageResponseSerializationFormat.ARROW) { + finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setEndBefore(options.getEndBefore()); + } } } else { finalOptions = options; } + if (finalOptions != null + && ModelHelper.resolveSerializationFormat(finalOptions.getStorageResponseSerializationFormat()) + == StorageResponseSerializationFormat.ARROW) { + return listBlobsFlatSegmentArrow(marker, finalOptions, timeout); + } + return listBlobsFlatSegment(marker, finalOptions, timeout).map(response -> { List value = response.getValue().getSegment() == null ? Collections.emptyList() @@ -1177,6 +1198,62 @@ PagedFlux listBlobsFlatWithOptionalTimeout(ListBlobsOptions options, S timeout); } + private Mono> listBlobsFlatSegmentArrow(String marker, ListBlobsOptions options, + Duration timeout) { + options = options == null ? new ListBlobsOptions() : options; + + ArrayList include + = options.getDetails().toList().isEmpty() ? null : options.getDetails().toList(); + + ListBlobsOptions finalOptions = options; + return StorageImplUtils.applyOptionalTimeout(this.azureBlobStorage.getContainers() + .listBlobFlatSegmentApacheArrowWithResponseAsync(containerName, finalOptions.getPrefix(), marker, + finalOptions.getMaxResultsPerPage(), include, null, finalOptions.getStartFrom(), + finalOptions.getEndBefore(), null, Context.NONE), + timeout).flatMap(response -> { + String contentType = response.getHeaders().getValue(com.azure.core.http.HttpHeaderName.CONTENT_TYPE); + + return FluxUtil.collectBytesInByteBufferStream(response.getValue()).map(bytes -> { + java.io.ByteArrayInputStream inputStream = new java.io.ByteArrayInputStream(bytes); + + if (StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM)) { + ArrowListBlobsResult arrowResult = ArrowBlobListDeserializer.deserialize(inputStream); + + List value = arrowResult.getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return (PagedResponse) new PagedResponseBase<>(response.getRequest(), + response.getStatusCode(), response.getHeaders(), value, arrowResult.getNextMarker(), + response.getDeserializedHeaders()); + } else { + // XML fallback + try { + ListBlobsFlatSegmentResponse xmlResponse + = ListBlobsFlatSegmentResponse.fromXml(com.azure.xml.XmlReader.fromStream(inputStream)); + + List value = xmlResponse.getSegment() == null + ? Collections.emptyList() + : xmlResponse.getSegment() + .getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return (PagedResponse) new PagedResponseBase<>(response.getRequest(), + response.getStatusCode(), response.getHeaders(), value, xmlResponse.getNextMarker(), + null); + } catch (javax.xml.stream.XMLStreamException e) { + throw LOGGER + .logExceptionAsError(new RuntimeException("Failed to parse XML fallback response", e)); + } + } + }); + }); + } + /** * Returns a reactive Publisher emitting all the blobs and directories (prefixes) under the given directory * (prefix). Directories will have {@link BlobItem#isPrefix()} set to true. @@ -1302,10 +1379,22 @@ PagedFlux listBlobsHierarchyWithOptionalTimeout(String delimiter, List .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; } + + if (finalOptions != null + && ModelHelper.resolveSerializationFormat(finalOptions.getStorageResponseSerializationFormat()) + == StorageResponseSerializationFormat.ARROW) { + return listBlobsHierarchySegmentArrow(marker, delimiter, finalOptions, timeout); + } + return listBlobsHierarchySegment(marker, delimiter, finalOptions, timeout).map(response -> { BlobHierarchyListSegment segment = response.getValue().getSegment(); List value; @@ -1344,6 +1433,71 @@ PagedFlux listBlobsHierarchyWithOptionalTimeout(String delimiter, List timeout); } + private Mono> listBlobsHierarchySegmentArrow(String marker, String delimiter, + ListBlobsOptions options, Duration timeout) { + options = options == null ? new ListBlobsOptions() : options; + if (options.getDetails().getRetrieveSnapshots()) { + throw LOGGER.logExceptionAsError( + new UnsupportedOperationException("Including snapshots in a hierarchical listing is not supported.")); + } + + ArrayList include + = options.getDetails().toList().isEmpty() ? null : options.getDetails().toList(); + + ListBlobsOptions finalOptions = options; + return StorageImplUtils + .applyOptionalTimeout(this.azureBlobStorage.getContainers() + .listBlobHierarchySegmentApacheArrowWithResponseAsync(containerName, delimiter, + finalOptions.getPrefix(), marker, finalOptions.getMaxResultsPerPage(), include, null, + finalOptions.getStartFrom(), finalOptions.getEndBefore(), null, Context.NONE), + timeout) + .flatMap(response -> { + String contentType = response.getHeaders().getValue(com.azure.core.http.HttpHeaderName.CONTENT_TYPE); + + return FluxUtil.collectBytesInByteBufferStream(response.getValue()).map(bytes -> { + java.io.ByteArrayInputStream inputStream = new java.io.ByteArrayInputStream(bytes); + + if (StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM)) { + ArrowListBlobsResult arrowResult = ArrowBlobListDeserializer.deserialize(inputStream); + + List value = arrowResult.getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return (PagedResponse) new PagedResponseBase<>(response.getRequest(), + response.getStatusCode(), response.getHeaders(), value, arrowResult.getNextMarker(), + response.getDeserializedHeaders()); + } else { + // XML fallback + try { + ListBlobsHierarchySegmentResponse xmlResponse = ListBlobsHierarchySegmentResponse + .fromXml(com.azure.xml.XmlReader.fromStream(inputStream)); + + BlobHierarchyListSegment segment = xmlResponse.getSegment(); + List value = new ArrayList<>(); + if (segment != null) { + segment.getBlobItems() + .forEach(item -> value.add(BlobItemConstructorProxy.create(item))); + segment.getBlobPrefixes() + .forEach(prefix -> value + .add(new BlobItem().setName(ModelHelper.toBlobNameString(prefix.getName())) + .setIsPrefix(true))); + } + + return (PagedResponse) new PagedResponseBase<>(response.getRequest(), + response.getStatusCode(), response.getHeaders(), value, xmlResponse.getNextMarker(), + null); + } catch (javax.xml.stream.XMLStreamException e) { + throw LOGGER + .logExceptionAsError(new RuntimeException("Failed to parse XML fallback response", e)); + } + } + }); + }); + } + /** * Returns a reactive Publisher emitting the blobs in this container whose tags match the query expression. For more * information, including information on the query syntax, see the Azure Docs. 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 64de81617f9c..696d95753dd4 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 @@ -25,12 +25,16 @@ import com.azure.storage.blob.implementation.models.ContainersGetAccessPolicyHeaders; import com.azure.storage.blob.implementation.models.ContainersGetAccountInfoHeaders; import com.azure.storage.blob.implementation.models.ContainersGetPropertiesHeaders; +import com.azure.storage.blob.implementation.models.ContainersListBlobFlatSegmentApacheArrowHeaders; import com.azure.storage.blob.implementation.models.ContainersListBlobFlatSegmentHeaders; +import com.azure.storage.blob.implementation.models.ContainersListBlobHierarchySegmentApacheArrowHeaders; import com.azure.storage.blob.implementation.models.ContainersListBlobHierarchySegmentHeaders; import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.FilterBlobSegment; import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse; import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer.ArrowListBlobsResult; import com.azure.storage.blob.implementation.util.BlobConstants; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; import com.azure.storage.blob.implementation.util.ModelHelper; @@ -47,6 +51,7 @@ import com.azure.storage.blob.models.ListBlobsOptions; import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; import com.azure.storage.blob.models.TaggedBlobItem; import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.blob.options.BlobContainerCreateOptions; @@ -54,9 +59,16 @@ import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.common.StorageSharedKeyCredential; import com.azure.storage.common.Utility; +import com.azure.storage.common.implementation.Constants; import com.azure.storage.common.implementation.SasImplUtils; import com.azure.storage.common.implementation.StorageImplUtils; +import com.azure.xml.XmlReader; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import javax.xml.stream.XMLStreamException; import java.net.URI; import java.time.Duration; import java.time.OffsetDateTime; @@ -1029,6 +1041,11 @@ public PagedIterable listBlobs(ListBlobsOptions options, String contin .setStartFrom(options.getStartFrom()) .setDetails(options.getDetails()); + if (options.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { + finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setEndBefore(options.getEndBefore()); + } + } /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. @@ -1040,26 +1057,79 @@ public PagedIterable listBlobs(ListBlobsOptions options, String contin ArrayList include = finalOptions.getDetails().toList().isEmpty() ? null : finalOptions.getDetails().toList(); - Callable> operation - = () -> this.azureBlobStorage.getContainers() - .listBlobFlatSegmentWithResponse(containerName, finalOptions.getPrefix(), nextMarker, - finalOptions.getMaxResultsPerPage(), include, finalOptions.getStartFrom(), null, null, - Context.NONE); - - ResponseBase response - = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); - - List value = response.getValue().getSegment() == null - ? Collections.emptyList() - : response.getValue() - .getSegment() - .getBlobItems() - .stream() - .map(ModelHelper::populateBlobItem) - .collect(Collectors.toList()); - - return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), - value, response.getValue().getNextMarker(), response.getDeserializedHeaders()); + if (finalOptions.getStorageResponseSerializationFormat() == StorageResponseSerializationFormat.ARROW) { + Callable> operation + = () -> this.azureBlobStorage.getContainers() + .listBlobFlatSegmentApacheArrowWithResponse(containerName, finalOptions.getPrefix(), nextMarker, + finalOptions.getMaxResultsPerPage(), include, null, finalOptions.getStartFrom(), + finalOptions.getEndBefore(), null, Context.NONE); + ResponseBase response + = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); + + String contentType = response.getHeaders().getValue(com.azure.core.http.HttpHeaderName.CONTENT_TYPE); + + // The response body is an InputStream backed by the network buffer. It must be closed to release the + // underlying buffer, otherwise the transport (e.g. Netty) will report a resource leak. + try (InputStream responseBody = response.getValue()) { + if (StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM)) { + // Arrow response — parse with Arrow parser entrypoint + ArrowListBlobsResult arrowResult = ArrowBlobListDeserializer.deserialize(responseBody); + + List value = arrowResult.getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), + response.getHeaders(), value, arrowResult.getNextMarker(), + response.getDeserializedHeaders()); + } else { + // XML fallback — service returned XML instead of Arrow + try { + ListBlobsFlatSegmentResponse xmlResponse + = ListBlobsFlatSegmentResponse.fromXml(XmlReader.fromStream(responseBody)); + + List value = xmlResponse.getSegment() == null + ? Collections.emptyList() + : xmlResponse.getSegment() + .getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), + response.getHeaders(), value, xmlResponse.getNextMarker(), null); + } catch (XMLStreamException e) { + throw LOGGER + .logExceptionAsError(new RuntimeException("Failed to parse XML fallback response", e)); + } + } + } catch (IOException e) { + throw LOGGER + .logExceptionAsError(new UncheckedIOException("Failed to close ListBlobs response stream.", e)); + } + } else { + Callable> operation + = () -> this.azureBlobStorage.getContainers() + .listBlobFlatSegmentWithResponse(containerName, finalOptions.getPrefix(), nextMarker, + finalOptions.getMaxResultsPerPage(), include, finalOptions.getStartFrom(), null, null, + Context.NONE); + ResponseBase response + = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); + + List value = response.getValue().getSegment() == null + ? Collections.emptyList() + : response.getValue() + .getSegment() + .getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), + value, response.getValue().getNextMarker(), response.getDeserializedHeaders()); + } }; return new PagedIterable<>(pageSize -> retriever.apply(continuationToken, pageSize), retriever); @@ -1164,6 +1234,11 @@ public PagedIterable listBlobsByHierarchy(String delimiter, ListBlobsO .setPrefix(options.getPrefix()) .setDetails(options.getDetails()) .setStartFrom(options.getStartFrom()); + if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) + == StorageResponseSerializationFormat.ARROW) { + finalOptions.setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setEndBefore(options.getEndBefore()); + } } /* If pageSize was not set in a .byPage(int) method, the page size from options will be preserved. @@ -1186,25 +1261,80 @@ private PagedResponse listBlobsHierarchySegment(String marker, String ArrayList include = options.getDetails().toList().isEmpty() ? null : options.getDetails().toList(); - Callable> operation - = () -> azureBlobStorage.getContainers() - .listBlobHierarchySegmentWithResponse(containerName, delimiter, options.getPrefix(), marker, - options.getMaxResultsPerPage(), include, options.getStartFrom(), null, null, Context.NONE); + if (ModelHelper.resolveSerializationFormat(options.getStorageResponseSerializationFormat()) + == StorageResponseSerializationFormat.ARROW) { + Callable> operation + = () -> azureBlobStorage.getContainers() + .listBlobHierarchySegmentApacheArrowWithResponse(containerName, delimiter, options.getPrefix(), + marker, options.getMaxResultsPerPage(), include, null, options.getStartFrom(), + options.getEndBefore(), null, Context.NONE); + ResponseBase response + = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); + + String contentType = response.getHeaders().getValue(com.azure.core.http.HttpHeaderName.CONTENT_TYPE); + + // The response body is an InputStream backed by the network buffer. It must be closed to release the + // underlying buffer, otherwise the transport (e.g. Netty) will report a resource leak. + try (InputStream responseBody = response.getValue()) { + if (StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM)) { + ArrowListBlobsResult arrowResult = ArrowBlobListDeserializer.deserialize(responseBody); + + List value = arrowResult.getBlobItems() + .stream() + .map(ModelHelper::populateBlobItem) + .collect(Collectors.toList()); + + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), + response.getHeaders(), value, arrowResult.getNextMarker(), response.getDeserializedHeaders()); + } else { + // XML fallback — service returned XML instead of Arrow + try { + ListBlobsHierarchySegmentResponse xmlResponse + = ListBlobsHierarchySegmentResponse.fromXml(XmlReader.fromStream(responseBody)); + + BlobHierarchyListSegment segment = xmlResponse.getSegment(); + List value = new ArrayList<>(); + if (segment != null) { + segment.getBlobItems().forEach(item -> value.add(BlobItemConstructorProxy.create(item))); + segment.getBlobPrefixes() + .forEach(prefix -> value + .add(new BlobItem().setName(ModelHelper.toBlobNameString(prefix.getName())) + .setIsPrefix(true))); + } + + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), + response.getHeaders(), value, xmlResponse.getNextMarker(), null); + } catch (XMLStreamException e) { + throw LOGGER + .logExceptionAsError(new RuntimeException("Failed to parse XML fallback response", e)); + } + } + } catch (IOException e) { + throw LOGGER + .logExceptionAsError(new UncheckedIOException("Failed to close ListBlobs response stream.", e)); + } + } else { + Callable> operation + = () -> azureBlobStorage.getContainers() + .listBlobHierarchySegmentWithResponse(containerName, delimiter, options.getPrefix(), marker, + options.getMaxResultsPerPage(), include, options.getStartFrom(), null, null, Context.NONE); - ResponseBase response - = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); + ResponseBase response + = StorageImplUtils.sendRequest(operation, timeout, BlobStorageException.class); - BlobHierarchyListSegment segment = response.getValue().getSegment(); - List value = new ArrayList<>(); - if (segment != null) { - segment.getBlobItems().forEach(item -> value.add(BlobItemConstructorProxy.create(item))); - segment.getBlobPrefixes() - .forEach(prefix -> value - .add(new BlobItem().setName(ModelHelper.toBlobNameString(prefix.getName())).setIsPrefix(true))); - } + BlobHierarchyListSegment segment = response.getValue().getSegment(); + List value = new ArrayList<>(); + if (segment != null) { + segment.getBlobItems().forEach(item -> value.add(BlobItemConstructorProxy.create(item))); + segment.getBlobPrefixes() + .forEach(prefix -> value + .add(new BlobItem().setName(ModelHelper.toBlobNameString(prefix.getName())).setIsPrefix(true))); + } - return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), value, - response.getValue().getNextMarker(), response.getDeserializedHeaders()); + return new PagedResponseBase<>(response.getRequest(), response.getStatusCode(), response.getHeaders(), + value, response.getValue().getNextMarker(), response.getDeserializedHeaders()); + } } /** diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobListArrowParseException.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobListArrowParseException.java new file mode 100644 index 000000000000..6d2465d26bff --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobListArrowParseException.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.models; + +/** + * Exception thrown when parsing Arrow ListBlobs payloads fails or encounters unsupported content. + */ +public final class BlobListArrowParseException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Creates an exception with a message. + * + * @param message parse failure details. + */ + public BlobListArrowParseException(String message) { + super(message); + } + + /** + * Creates an exception with a message and cause. + * + * @param message parse failure details. + * @param cause originating cause. + */ + public BlobListArrowParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java index 7dd424fe666c..1d409a5a4cdd 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/BlobsDownloadHeaders.java @@ -295,30 +295,6 @@ public final class BlobsDownloadHeaders { @Generated private Long xMsStructuredContentLength; - /* - * The x-ms-access-tier property. - */ - @Generated - private String xMsAccessTier; - - /* - * The x-ms-access-tier-inferred property. - */ - @Generated - private Boolean xMsAccessTierInferred; - - /* - * The x-ms-access-tier-change-time property. - */ - @Generated - private DateTimeRfc1123 xMsAccessTierChangeTime; - - /* - * The x-ms-smart-access-tier property. - */ - @Generated - private String xMsSmartAccessTier; - /* * The x-ms-content-crc64 property. */ @@ -391,16 +367,6 @@ public final class BlobsDownloadHeaders { private static final HttpHeaderName X_MS_STRUCTURED_CONTENT_LENGTH = HttpHeaderName.fromString("x-ms-structured-content-length"); - private static final HttpHeaderName X_MS_ACCESS_TIER = HttpHeaderName.fromString("x-ms-access-tier"); - - private static final HttpHeaderName X_MS_ACCESS_TIER_INFERRED - = HttpHeaderName.fromString("x-ms-access-tier-inferred"); - - private static final HttpHeaderName X_MS_ACCESS_TIER_CHANGE_TIME - = HttpHeaderName.fromString("x-ms-access-tier-change-time"); - - private static final HttpHeaderName X_MS_SMART_ACCESS_TIER = HttpHeaderName.fromString("x-ms-smart-access-tier"); - private static final HttpHeaderName X_MS_CONTENT_CRC64 = HttpHeaderName.fromString("x-ms-content-crc64"); // HttpHeaders containing the raw property values. @@ -563,20 +529,6 @@ public BlobsDownloadHeaders(HttpHeaders rawHeaders) { } else { this.xMsStructuredContentLength = null; } - this.xMsAccessTier = rawHeaders.getValue(X_MS_ACCESS_TIER); - String xMsAccessTierInferred = rawHeaders.getValue(X_MS_ACCESS_TIER_INFERRED); - if (xMsAccessTierInferred != null) { - this.xMsAccessTierInferred = Boolean.parseBoolean(xMsAccessTierInferred); - } else { - this.xMsAccessTierInferred = null; - } - String xMsAccessTierChangeTime = rawHeaders.getValue(X_MS_ACCESS_TIER_CHANGE_TIME); - if (xMsAccessTierChangeTime != null) { - this.xMsAccessTierChangeTime = new DateTimeRfc1123(xMsAccessTierChangeTime); - } else { - this.xMsAccessTierChangeTime = null; - } - this.xMsSmartAccessTier = rawHeaders.getValue(X_MS_SMART_ACCESS_TIER); String xMsContentCrc64 = rawHeaders.getValue(X_MS_CONTENT_CRC64); if (xMsContentCrc64 != null) { this.xMsContentCrc64 = Base64.getDecoder().decode(xMsContentCrc64); @@ -1632,101 +1584,6 @@ public BlobsDownloadHeaders setXMsStructuredContentLength(Long xMsStructuredCont return this; } - /** - * Get the xMsAccessTier property: The x-ms-access-tier property. - * - * @return the xMsAccessTier value. - */ - @Generated - public String getXMsAccessTier() { - return this.xMsAccessTier; - } - - /** - * Set the xMsAccessTier property: The x-ms-access-tier property. - * - * @param xMsAccessTier the xMsAccessTier value to set. - * @return the BlobsDownloadHeaders object itself. - */ - @Generated - public BlobsDownloadHeaders setXMsAccessTier(String xMsAccessTier) { - this.xMsAccessTier = xMsAccessTier; - return this; - } - - /** - * Get the xMsAccessTierInferred property: The x-ms-access-tier-inferred property. - * - * @return the xMsAccessTierInferred value. - */ - @Generated - public Boolean isXMsAccessTierInferred() { - return this.xMsAccessTierInferred; - } - - /** - * Set the xMsAccessTierInferred property: The x-ms-access-tier-inferred property. - * - * @param xMsAccessTierInferred the xMsAccessTierInferred value to set. - * @return the BlobsDownloadHeaders object itself. - */ - @Generated - public BlobsDownloadHeaders setXMsAccessTierInferred(Boolean xMsAccessTierInferred) { - this.xMsAccessTierInferred = xMsAccessTierInferred; - return this; - } - - /** - * Get the xMsAccessTierChangeTime property: The x-ms-access-tier-change-time property. - * - * @return the xMsAccessTierChangeTime value. - */ - @Generated - public OffsetDateTime getXMsAccessTierChangeTime() { - if (this.xMsAccessTierChangeTime == null) { - return null; - } - return this.xMsAccessTierChangeTime.getDateTime(); - } - - /** - * Set the xMsAccessTierChangeTime property: The x-ms-access-tier-change-time property. - * - * @param xMsAccessTierChangeTime the xMsAccessTierChangeTime value to set. - * @return the BlobsDownloadHeaders object itself. - */ - @Generated - public BlobsDownloadHeaders setXMsAccessTierChangeTime(OffsetDateTime xMsAccessTierChangeTime) { - if (xMsAccessTierChangeTime == null) { - this.xMsAccessTierChangeTime = null; - } else { - this.xMsAccessTierChangeTime = new DateTimeRfc1123(xMsAccessTierChangeTime); - } - return this; - } - - /** - * Get the xMsSmartAccessTier property: The x-ms-smart-access-tier property. - * - * @return the xMsSmartAccessTier value. - */ - @Generated - public String getXMsSmartAccessTier() { - return this.xMsSmartAccessTier; - } - - /** - * Set the xMsSmartAccessTier property: The x-ms-smart-access-tier property. - * - * @param xMsSmartAccessTier the xMsSmartAccessTier value to set. - * @return the BlobsDownloadHeaders object itself. - */ - @Generated - public BlobsDownloadHeaders setXMsSmartAccessTier(String xMsSmartAccessTier) { - this.xMsSmartAccessTier = xMsSmartAccessTier; - return this; - } - /** * Get the xMsContentCrc64 property: The x-ms-content-crc64 property. * diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializer.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializer.java new file mode 100644 index 000000000000..ad46d85ce175 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializer.java @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.storage.blob.implementation.models.BlobItemInternal; +import com.azure.storage.blob.implementation.models.BlobItemPropertiesInternal; +import com.azure.storage.blob.implementation.models.BlobListArrowParseException; +import com.azure.storage.blob.implementation.models.BlobName; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.Batch; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.BoolColumn; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.Column; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.IntColumn; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.MapColumn; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.DecodedArrowStream; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.StringColumn; +import com.azure.storage.blob.implementation.util.BlobListArrowStreamReader.TimestampColumn; +import com.azure.storage.blob.models.AccessTier; +import com.azure.storage.blob.models.ArchiveStatus; +import com.azure.storage.blob.models.BlobImmutabilityPolicyMode; +import com.azure.storage.blob.models.BlobType; +import com.azure.storage.blob.models.CopyStatusType; +import com.azure.storage.blob.models.LeaseDurationType; +import com.azure.storage.blob.models.LeaseStateType; +import com.azure.storage.blob.models.LeaseStatusType; +import com.azure.storage.blob.models.RehydratePriority; + +import java.io.InputStream; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Deserializes an Apache Arrow IPC stream from the ListBlobs response into a list of {@link BlobItemInternal} objects. + */ +public final class ArrowBlobListDeserializer { + + /** + * The authoritative set of Arrow column names this deserializer knows how to read. It must mirror exactly the + * field names requested by {@link #readRow(Batch, int)}; the {@code alltypes} golden fixture drift-guard test + * asserts they stay in sync. Any column present in the response schema but absent from this set means the service + * has emitted a field this SDK version does not understand, which {@link #validateKnownColumns(Batch)} rejects. + */ + private static final Set KNOWN_COLUMNS = Collections.unmodifiableSet(new HashSet<>( + Arrays.asList("Name", "ResourceType", "Deleted", "Snapshot", "VersionId", "IsCurrentVersion", "HasVersionsOnly", + "Metadata", "OrMetadata", "Tags", "Creation-Time", "Last-Modified", "Etag", "Content-Length", + "Content-Type", "Content-Encoding", "Content-Language", "Content-Disposition", "Cache-Control", + "Content-MD5", "BlobType", "AccessTier", "AccessTierInferred", "AccessTierChangeTime", "SmartAccessTier", + "LeaseStatus", "LeaseState", "LeaseDuration", "ServerEncrypted", "CustomerProvidedKeySha256", + "EncryptionScope", "IncrementalCopy", "CopyId", "CopyStatus", "CopySource", "CopyProgress", + "CopyCompletionTime", "CopyStatusDescription", "CopyDestinationSnapshot", "x-ms-blob-sequence-number", + "Sealed", "LegalHold", "DeletedTime", "LastAccessTime", "ImmutabilityPolicyUntilDate", + "ImmutabilityPolicyMode", "ArchiveStatus", "RehydratePriority", "TagCount", "RemainingRetentionDays"))); + + private ArrowBlobListDeserializer() { + } + + /** + * Exposes the authoritative set of columns this deserializer understands, for the drift-guard test that asserts it + * stays in sync with the full-schema golden fixture. Package-private: not part of the public API. + * + * @return the immutable set of known column names + */ + static Set knownColumns() { + return KNOWN_COLUMNS; + } + + /** + * Deserializes an Arrow IPC stream into blob items and pagination metadata. + * + * @param arrowStream the Arrow IPC input stream from the service response + * @return the deserialized result containing blob items and next marker + * @throws BlobListArrowParseException if deserialization fails + */ + public static ArrowListBlobsResult deserialize(InputStream arrowStream) { + if (arrowStream == null) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: input stream is null."); + } + + List results = new ArrayList<>(); + String nextMarker = null; + Integer numberOfRecords = null; + + DecodedArrowStream decodedArrowStream = BlobListArrowStreamReader.read(arrowStream); + + Map schemaMetadata = decodedArrowStream.getSchemaMetadata(); + if (schemaMetadata != null) { + nextMarker = schemaMetadata.get("NextMarker"); + if (nextMarker != null && nextMarker.isEmpty()) { + nextMarker = null; + } + + String numberOfRecordsStr = schemaMetadata.get("NumberOfRecords"); + if (numberOfRecordsStr != null && !numberOfRecordsStr.isEmpty()) { + try { + numberOfRecords = Integer.parseInt(numberOfRecordsStr); + } catch (NumberFormatException e) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: schema metadata 'NumberOfRecords' isn't a valid integer.", e); + } + } + } + + for (Batch batch : decodedArrowStream.getBatches()) { + validateKnownColumns(batch); + int rowCount = batch.getRowCount(); + for (int i = 0; i < rowCount; i++) { + results.add(readRow(batch, i)); + } + } + + return new ArrowListBlobsResult(results, nextMarker, numberOfRecords); + } + + /** + * Fails loudly if the batch schema contains any column this deserializer does not handle, so that a newly added + * service field surfaces as an error instead of being silently dropped. + * + * @param batch the record batch to validate + * @throws BlobListArrowParseException if the batch contains one or more columns absent from {@link #KNOWN_COLUMNS} + */ + private static void validateKnownColumns(Batch batch) { + List unknownColumns = null; + for (String columnName : batch.getColumnNames()) { + if (!KNOWN_COLUMNS.contains(columnName)) { + if (unknownColumns == null) { + unknownColumns = new ArrayList<>(); + } + unknownColumns.add(columnName); + } + } + + if (unknownColumns != null) { + Collections.sort(unknownColumns); + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: response contains unhandled column(s) " + unknownColumns + "."); + } + } + + private static BlobItemInternal readRow(Batch batch, int index) { + BlobItemInternal item = new BlobItemInternal(); + + String name = getVarChar(batch, "Name", index); + if (name != null) { + item.setName(new BlobName().setContent(name)); + } + + String resourceType = getVarChar(batch, "ResourceType", index); + if ("blobprefix".equals(resourceType)) { + item.setIsPrefix(true); + return item; + } + + BlobItemPropertiesInternal properties = new BlobItemPropertiesInternal(); + + Boolean deleted = getBit(batch, "Deleted", index); + if (deleted != null) { + item.setDeleted(deleted); + } + + item.setSnapshot(getVarChar(batch, "Snapshot", index)); + item.setVersionId(getVarChar(batch, "VersionId", index)); + item.setIsCurrentVersion(getBit(batch, "IsCurrentVersion", index)); + item.setHasVersionsOnly(getBit(batch, "HasVersionsOnly", index)); + + Map metadata = getMap(batch, "Metadata", index); + if (metadata != null) { + item.setMetadata(metadata); + } + + Map orMetadata = getMap(batch, "OrMetadata", index); + if (orMetadata != null) { + item.setObjectReplicationMetadata(orMetadata); + } + + Map tags = getMap(batch, "Tags", index); + if (tags != null) { + item.setBlobTags(ModelHelper.toBlobTags(tags)); + } + + properties.setCreationTime(getTimestamp(batch, "Creation-Time", index)); + properties.setLastModified(getTimestamp(batch, "Last-Modified", index)); + properties.setETag(getVarChar(batch, "Etag", index)); + properties.setContentLength(getUInt64(batch, "Content-Length", index)); + properties.setContentType(getVarChar(batch, "Content-Type", index)); + properties.setContentEncoding(getVarChar(batch, "Content-Encoding", index)); + properties.setContentLanguage(getVarChar(batch, "Content-Language", index)); + properties.setContentDisposition(getVarChar(batch, "Content-Disposition", index)); + properties.setCacheControl(getVarChar(batch, "Cache-Control", index)); + + String contentMd5 = getVarChar(batch, "Content-MD5", index); + if (contentMd5 != null) { + properties.setContentMd5(Base64.getDecoder().decode(contentMd5)); + } + + String blobType = getVarChar(batch, "BlobType", index); + if (blobType != null) { + properties.setBlobType(BlobType.fromString(blobType)); + } + + String accessTier = getVarChar(batch, "AccessTier", index); + if (accessTier != null) { + properties.setAccessTier(AccessTier.fromString(accessTier)); + } + properties.setAccessTierInferred(getBit(batch, "AccessTierInferred", index)); + properties.setAccessTierChangeTime(getTimestamp(batch, "AccessTierChangeTime", index)); + + String smartAccessTier = getVarChar(batch, "SmartAccessTier", index); + if (smartAccessTier != null) { + properties.setSmartAccessTier(AccessTier.fromString(smartAccessTier)); + } + + String leaseStatus = getVarChar(batch, "LeaseStatus", index); + if (leaseStatus != null) { + properties.setLeaseStatus(LeaseStatusType.fromString(leaseStatus)); + } + String leaseState = getVarChar(batch, "LeaseState", index); + if (leaseState != null) { + properties.setLeaseState(LeaseStateType.fromString(leaseState)); + } + String leaseDuration = getVarChar(batch, "LeaseDuration", index); + if (leaseDuration != null) { + properties.setLeaseDuration(LeaseDurationType.fromString(leaseDuration)); + } + + properties.setServerEncrypted(getBit(batch, "ServerEncrypted", index)); + properties.setCustomerProvidedKeySha256(getVarChar(batch, "CustomerProvidedKeySha256", index)); + properties.setEncryptionScope(getVarChar(batch, "EncryptionScope", index)); + properties.setIncrementalCopy(getBit(batch, "IncrementalCopy", index)); + + properties.setCopyId(getVarChar(batch, "CopyId", index)); + String copyStatus = getVarChar(batch, "CopyStatus", index); + if (copyStatus != null) { + properties.setCopyStatus(CopyStatusType.fromString(copyStatus)); + } + properties.setCopySource(getVarChar(batch, "CopySource", index)); + properties.setCopyProgress(getVarChar(batch, "CopyProgress", index)); + properties.setCopyCompletionTime(getTimestamp(batch, "CopyCompletionTime", index)); + properties.setCopyStatusDescription(getVarChar(batch, "CopyStatusDescription", index)); + properties.setDestinationSnapshot(getVarChar(batch, "CopyDestinationSnapshot", index)); + + properties.setBlobSequenceNumber(getUInt64(batch, "x-ms-blob-sequence-number", index)); + + properties.setIsSealed(getBit(batch, "Sealed", index)); + properties.setLegalHold(getBit(batch, "LegalHold", index)); + properties.setDeletedTime(getTimestamp(batch, "DeletedTime", index)); + properties.setLastAccessedOn(getTimestamp(batch, "LastAccessTime", index)); + properties.setImmutabilityPolicyExpiresOn(getTimestamp(batch, "ImmutabilityPolicyUntilDate", index)); + + String immutabilityMode = getVarChar(batch, "ImmutabilityPolicyMode", index); + if (immutabilityMode != null) { + properties.setImmutabilityPolicyMode(BlobImmutabilityPolicyMode.fromString(immutabilityMode)); + } + + String archiveStatus = getVarChar(batch, "ArchiveStatus", index); + if (archiveStatus != null) { + properties.setArchiveStatus(ArchiveStatus.fromString(archiveStatus)); + } + + String rehydratePriority = getVarChar(batch, "RehydratePriority", index); + if (rehydratePriority != null) { + properties.setRehydratePriority(RehydratePriority.fromString(rehydratePriority)); + } + + Long tagCount = getUInt64(batch, "TagCount", index); + if (tagCount != null) { + properties.setTagCount(tagCount.intValue()); + } + Long remainingRetentionDays = getUInt64(batch, "RemainingRetentionDays", index); + if (remainingRetentionDays != null) { + properties.setRemainingRetentionDays(remainingRetentionDays.intValue()); + } + + item.setProperties(properties); + return item; + } + + private static T getColumn(Batch batch, String name, int index, Class type, + String typeLabel) { + Column column = batch.getColumn(name); + if (column == null || column.isNull(index)) { + return null; + } + if (!type.isInstance(column)) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + name + "' has unsupported " + + typeLabel + " column type '" + column.getClass().getSimpleName() + "'."); + } + return type.cast(column); + } + + // region Arrow helpers + + private static String getVarChar(Batch batch, String name, int index) { + StringColumn column = getColumn(batch, name, index, StringColumn.class, "string"); + return column == null ? null : column.get(index); + } + + private static Long getUInt64(Batch batch, String name, int index) { + IntColumn column = getColumn(batch, name, index, IntColumn.class, "integer"); + return column == null ? null : column.get(index); + } + + private static Boolean getBit(Batch batch, String name, int index) { + BoolColumn column = getColumn(batch, name, index, BoolColumn.class, "boolean"); + return column == null ? null : column.get(index); + } + + private static OffsetDateTime getTimestamp(Batch batch, String name, int index) { + TimestampColumn column = getColumn(batch, name, index, TimestampColumn.class, "timestamp"); + if (column == null) { + return null; + } + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(column.getEpochSeconds(index)), ZoneOffset.UTC); + } + + private static Map getMap(Batch batch, String name, int index) { + MapColumn column = getColumn(batch, name, index, MapColumn.class, "map"); + return column == null ? null : column.get(index); + } + + /** + * Result of deserializing an Arrow ListBlobs response. + */ + public static final class ArrowListBlobsResult { + private final List blobItems; + private final String nextMarker; + private final Integer numberOfRecords; + + /** + * Creates an ArrowListBlobsResult. + * + * @param blobItems the deserialized blob items + * @param nextMarker the continuation token for the next page, or null if this is the last page + * @param numberOfRecords the total number of records reported by the service, or null if not present + */ + public ArrowListBlobsResult(List blobItems, String nextMarker, Integer numberOfRecords) { + this.blobItems = blobItems; + this.nextMarker = nextMarker; + this.numberOfRecords = numberOfRecords; + } + + /** + * @return the deserialized blob items + */ + public List getBlobItems() { + return blobItems; + } + + /** + * @return the continuation token for the next page, or null if this is the last page + */ + public String getNextMarker() { + return nextMarker; + } + + /** + * @return the total number of records reported by the service, or null if not present + */ + public Integer getNumberOfRecords() { + return numberOfRecords; + } + } + + //endregion +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java new file mode 100644 index 000000000000..8db6c0b24556 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java @@ -0,0 +1,570 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.storage.blob.implementation.models.BlobListArrowParseException; +import com.azure.storage.blob.implementation.util.apachearrow.Buffer; +import com.azure.storage.blob.implementation.util.apachearrow.Endianness; +import com.azure.storage.blob.implementation.util.apachearrow.Field; +import com.azure.storage.blob.implementation.util.apachearrow.FieldNode; +import com.azure.storage.blob.implementation.util.apachearrow.Int; +import com.azure.storage.blob.implementation.util.apachearrow.KeyValue; +import com.azure.storage.blob.implementation.util.apachearrow.Message; +import com.azure.storage.blob.implementation.util.apachearrow.MessageHeader; +import com.azure.storage.blob.implementation.util.apachearrow.RecordBatch; +import com.azure.storage.blob.implementation.util.apachearrow.Schema; +import com.azure.storage.blob.implementation.util.apachearrow.TimeUnit; +import com.azure.storage.blob.implementation.util.apachearrow.Timestamp; +import com.azure.storage.blob.implementation.util.apachearrow.Type; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Minimal Apache Arrow IPC stream reader scoped to the needs of the ListBlobs Arrow response. + *

+ * This reader intentionally supports only the subset of the Arrow IPC format emitted by the Storage ListBlobs + * endpoint: a single schema message followed by zero or more uncompressed, little-endian record batches whose + * columns are UTF-8 strings, booleans, integers, second-precision timestamps, and map<string,string>. Anything + * outside that subset (dictionaries, compression, big-endian, unsupported types) fails fast with + * {@link BlobListArrowParseException}. + *

+ * It depends only on the {@code arrow-format} flatbuffer definitions for metadata decoding and reads record batch + * bodies directly, so it does not require the {@code arrow-vector} runtime. + */ +final class BlobListArrowStreamReader { + // This is hex representation of the maximum value of an unsigned 32-bit integer. + private static final int CONTINUATION_MARKER = 0xFFFFFFFF; + + private BlobListArrowStreamReader() { + } + + /** + * The decoded contents of an Arrow IPC stream. + */ + static final class DecodedArrowStream { + private final Map schemaMetadata; + private final List batches; + + DecodedArrowStream(Map schemaMetadata, List batches) { + this.schemaMetadata = schemaMetadata; + this.batches = batches; + } + + Map getSchemaMetadata() { + return schemaMetadata; + } + + List getBatches() { + return batches; + } + } + + /** + * A single decoded record batch: a row count and columns addressable by field name. + */ + static final class Batch { + private final int rowCount; + private final Map columns; + + Batch(int rowCount, Map columns) { + this.rowCount = rowCount; + this.columns = columns; + } + + int getRowCount() { + return rowCount; + } + + Column getColumn(String name) { + return columns.get(name); + } + + Set getColumnNames() { + return Collections.unmodifiableSet(columns.keySet()); + } + } + + /** + * Reads and decodes an Arrow IPC stream. + * + * @param stream the Arrow IPC stream. + * @return the decoded schema metadata and record batches. + * @throws BlobListArrowParseException if the stream is malformed or uses an unsupported feature. + */ + static DecodedArrowStream read(InputStream stream) { + byte[] bytes; + try { + bytes = readAll(stream); + } catch (IOException e) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: unable to read IPC stream.", e); + } + + ByteBuffer steamBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + Map schemaMetadata = null; + List fields = null; + List batches = new ArrayList<>(); + + int pos = 0; + int length = bytes.length; + while (atLeastFourBytesRemaining(pos, length)) { + int marker = steamBuffer.getInt(pos); + pos += 4; + + int metadataLength; + if (isModernStream(marker)) { + if (pos + 4 > length) { + break; + } + metadataLength = steamBuffer.getInt(pos); + pos += 4; + } else { + // Pre-0.15 streams used a bare length prefix without the continuation marker. + metadataLength = marker; + } + + if (metadataLength == 0) { + // End-of-stream marker. + break; + } + if (isMessageOutOfBounds(metadataLength, pos, length)) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: message metadata length is out of bounds."); + } + + ByteBuffer messageBuffer + = ByteBuffer.wrap(bytes, pos, metadataLength).slice().order(ByteOrder.LITTLE_ENDIAN); + Message message = Message.getRootAsMessage(messageBuffer); + pos += metadataLength; + + long bodyLength = message.bodyLength(); + if (isMessageOutOfBounds(bodyLength, pos, length)) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: message body length is out of bounds."); + } + int bodyStart = pos; + pos += (int) bodyLength; + + byte headerType = message.headerType(); + if (headerType == MessageHeader.SCHEMA) { + Schema schema = requireHeader((Schema) message.header(new Schema()), "schema"); + if (schema.endianness() != Endianness.LITTLE) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: only little-endian streams are supported."); + } + schemaMetadata = readKeyValueMetadata(schema); + fields = readFields(schema); + } else if (headerType == MessageHeader.RECORD_BATCH) { + if (fields == null) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: record batch encountered before schema."); + } + RecordBatch recordBatch + = requireHeader((RecordBatch) message.header(new RecordBatch()), "record batch"); + if (recordBatch.compression() != null) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: compressed record batches are not supported."); + } + batches.add(buildBatch(fields, recordBatch, steamBuffer, bodyStart)); + } else if (headerType == MessageHeader.DICTIONARY_BATCH) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: dictionary-encoded streams are not supported."); + } + // Other header types (NONE, and the commented-out Tensor/SparseTensor members of the Arrow MessageHeader + // union) are not expected in a ListBlobs response and are ignored. See MessageHeader for why those two + // constants are kept (commented out) and how to revive them if the service ever starts emitting them. + } + + if (fields == null) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: stream contained no schema."); + } + + Map finalSchemaMetadata = schemaMetadata == null ? new HashMap<>() : schemaMetadata; + return new DecodedArrowStream(finalSchemaMetadata, batches); + } + + private static boolean isMessageOutOfBounds(long bodyLength, int pos, int length) { + return bodyLength < 0 || pos + bodyLength > length; + } + + private static boolean isModernStream(int marker) { + return marker == CONTINUATION_MARKER; + } + + private static boolean atLeastFourBytesRemaining(int pos, int length) { + return pos + 4 <= length; + } + + private static T requireHeader(T header, String description) { + if (header == null) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: " + description + " message header is missing."); + } + return header; + } + + private static Batch buildBatch(List fields, RecordBatch recordBatch, ByteBuffer body, int bodyStart) { + BatchCursor cursor = new BatchCursor(recordBatch, body, bodyStart); + Map columns = new LinkedHashMap<>(); + for (ArrowField field : fields) { + columns.put(field.name, buildColumn(field, cursor)); + } + return new Batch((int) recordBatch.length(), columns); + } + + private static Column buildColumn(ArrowField field, BatchCursor cursor) { + FieldNode node = cursor.nextNode(); + int valueCount = (int) node.length(); + + switch (field.typeType) { + case Type.UTF8: + case Type.BINARY: + return new StringColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.nextBuffer(), + cursor.body, cursor.bodyStart); + + case Type.BOOL: + return new BoolColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.body, + cursor.bodyStart); + + case Type.INT: + return new IntColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), field.bitWidth, field.signed, + cursor.body, cursor.bodyStart); + + case Type.TIMESTAMP: + return new TimestampColumn(valueCount, cursor.nextBuffer(), cursor.nextBuffer(), cursor.body, + cursor.bodyStart); + + case Type.MAP: + BufferRegion mapValidity = cursor.nextBuffer(); + BufferRegion mapOffsets = cursor.nextBuffer(); + // Map has a single Struct child ("entries") with key and value children. + ArrowField mapEntries = field.children.get(0); + StructColumn mapStruct = (StructColumn) buildColumn(mapEntries, cursor); + Column mapKeyColumn = mapStruct.children.get(0); + Column mapValueColumn = mapStruct.children.get(1); + if (!(mapKeyColumn instanceof StringColumn) || !(mapValueColumn instanceof StringColumn)) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name + + "' map entries must be string keys and values."); + } + return new MapColumn(valueCount, mapValidity, mapOffsets, (StringColumn) mapKeyColumn, + (StringColumn) mapValueColumn, cursor.body, cursor.bodyStart); + + case Type.STRUCT: + cursor.nextBuffer(); // struct validity buffer + List structChildren = new ArrayList<>(field.children.size()); + for (ArrowField child : field.children) { + structChildren.add(buildColumn(child, cursor)); + } + return new StructColumn(structChildren); + + default: + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name + + "' has unsupported Arrow type '" + Type.name(field.typeType) + "'."); + } + } + + private static List readFields(Schema schema) { + int count = schema.fieldsLength(); + List fields = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + fields.add(readField(schema.fields(i))); + } + return fields; + } + + private static ArrowField readField(Field field) { + String name = field.name(); + byte typeType = field.typeType(); + int bitWidth = 0; + boolean signed = false; + + if (typeType == Type.INT) { + Int intType = (Int) field.type(new Int()); + if (intType != null) { + bitWidth = intType.bitWidth(); + signed = intType.isSigned(); + } + } else if (typeType == Type.TIMESTAMP) { + Timestamp timestamp = (Timestamp) field.type(new Timestamp()); + if (timestamp != null && timestamp.unit() != TimeUnit.SECOND) { + throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + name + + "' uses an unsupported timestamp unit '" + TimeUnit.name(timestamp.unit()) + "'."); + } + } + + int childCount = field.childrenLength(); + List children = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + children.add(readField(field.children(i))); + } + return new ArrowField(name, typeType, bitWidth, signed, children); + } + + private static Map readKeyValueMetadata(Schema schema) { + int count = schema.customMetadataLength(); + if (count == 0) { + return new HashMap<>(); + } + Map metadata = new HashMap<>(); + for (int i = 0; i < count; i++) { + KeyValue keyValue = schema.customMetadata(i); + metadata.put(keyValue.key(), keyValue.value()); + } + return metadata; + } + + private static byte[] readAll(InputStream stream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int read; + while ((read = stream.read(chunk)) != -1) { + buffer.write(chunk, 0, read); + } + return buffer.toByteArray(); + } + + /** + * A parsed schema field with the minimal type information required for decoding. + */ + private static final class ArrowField { + private final String name; + private final byte typeType; + private final int bitWidth; + private final boolean signed; + private final List children; + + ArrowField(String name, byte typeType, int bitWidth, boolean signed, List children) { + this.name = name; + this.typeType = typeType; + this.bitWidth = bitWidth; + this.signed = signed; + this.children = children; + } + } + + /** + * Sequentially hands out the field nodes and buffers of a record batch in pre-order. + */ + private static final class BatchCursor { + private final RecordBatch recordBatch; + private final ByteBuffer body; + private final int bodyStart; + private int nodeIndex; + private int bufferIndex; + + BatchCursor(RecordBatch recordBatch, ByteBuffer body, int bodyStart) { + this.recordBatch = recordBatch; + this.body = body; + this.bodyStart = bodyStart; + } + + FieldNode nextNode() { + if (nodeIndex >= recordBatch.nodesLength()) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: record batch is missing expected field nodes."); + } + return recordBatch.nodes(nodeIndex++); + } + + BufferRegion nextBuffer() { + if (bufferIndex >= recordBatch.buffersLength()) { + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: record batch is missing expected buffers."); + } + Buffer buffer = recordBatch.buffers(bufferIndex++); + return new BufferRegion(buffer.offset(), buffer.length()); + } + } + + /** + * Offset and length of a single buffer within the record batch body. + */ + private static final class BufferRegion { + private final long offset; + private final long length; + + BufferRegion(long offset, long length) { + this.offset = offset; + this.length = length; + } + } + + /** + * Base class for decoded columns. + */ + abstract static class Column { + final int valueCount; + final BufferRegion validity; + final ByteBuffer body; + final int bodyStart; + + Column(int valueCount, BufferRegion validity, ByteBuffer body, int bodyStart) { + this.valueCount = valueCount; + this.validity = validity; + this.body = body; + this.bodyStart = bodyStart; + } + + boolean isNull(int index) { + if (validity == null || validity.length == 0) { + return false; + } + int bytePosition = bodyStart + (int) validity.offset + (index >> 3); + int bit = (body.get(bytePosition) >> (index & 7)) & 1; + return bit == 0; + } + } + + /** + * UTF-8 string column (Arrow Utf8/Binary). + */ + static final class StringColumn extends Column { + private final BufferRegion offsets; + private final BufferRegion data; + + StringColumn(int valueCount, BufferRegion validity, BufferRegion offsets, BufferRegion data, ByteBuffer body, + int bodyStart) { + super(valueCount, validity, body, bodyStart); + this.offsets = offsets; + this.data = data; + } + + String get(int index) { + int start = body.getInt(bodyStart + (int) offsets.offset + index * 4); + int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4); + int dataStart = bodyStart + (int) data.offset + start; + byte[] valueBytes = new byte[end - start]; + for (int i = 0; i < valueBytes.length; i++) { + valueBytes[i] = body.get(dataStart + i); + } + return new String(valueBytes, StandardCharsets.UTF_8); + } + } + + /** + * Boolean column stored as a bitmap (Arrow Bool). + */ + static final class BoolColumn extends Column { + private final BufferRegion data; + + BoolColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) { + super(valueCount, validity, body, bodyStart); + this.data = data; + } + + boolean get(int index) { + int bytePosition = bodyStart + (int) data.offset + (index >> 3); + int bit = (body.get(bytePosition) >> (index & 7)) & 1; + return bit == 1; + } + } + + /** + * Integer column (Arrow Int) of width 8/16/32/64, signed or unsigned, returned as a long. + */ + static final class IntColumn extends Column { + private final BufferRegion data; + private final int bitWidth; + private final boolean signed; + + IntColumn(int valueCount, BufferRegion validity, BufferRegion data, int bitWidth, boolean signed, + ByteBuffer body, int bodyStart) { + super(valueCount, validity, body, bodyStart); + this.data = data; + this.bitWidth = bitWidth; + this.signed = signed; + } + + long get(int index) { + int base = bodyStart + (int) data.offset; + switch (bitWidth) { + case 64: + return body.getLong(base + index * 8); + + case 32: + int value32 = body.getInt(base + index * 4); + return signed ? value32 : (value32 & 0xFFFFFFFFL); + + case 16: + short value16 = body.getShort(base + index * 2); + return signed ? value16 : (value16 & 0xFFFF); + + case 8: + byte value8 = body.get(base + index); + return signed ? value8 : (value8 & 0xFF); + + default: + throw new BlobListArrowParseException( + "ListBlobs Arrow parse failure: unsupported integer bit width '" + bitWidth + "'."); + } + } + } + + /** + * Second-precision timestamp column (Arrow Timestamp, SECOND unit). + */ + static final class TimestampColumn extends Column { + private final BufferRegion data; + + TimestampColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) { + super(valueCount, validity, body, bodyStart); + this.data = data; + } + + long getEpochSeconds(int index) { + return body.getLong(bodyStart + (int) data.offset + index * 8); + } + } + + /** + * Struct column holding ordered child columns (used internally for map entries). + */ + static final class StructColumn extends Column { + private final List children; + + StructColumn(List children) { + super(0, null, null, 0); + this.children = children; + } + } + + /** + * Map<string,string> column (Arrow Map of Struct<key:utf8,value:utf8>). + */ + static final class MapColumn extends Column { + private final BufferRegion offsets; + private final StringColumn keys; + private final StringColumn values; + + MapColumn(int valueCount, BufferRegion validity, BufferRegion offsets, StringColumn keys, StringColumn values, + ByteBuffer body, int bodyStart) { + super(valueCount, validity, body, bodyStart); + this.offsets = offsets; + this.keys = keys; + this.values = values; + } + + Map get(int index) { + int start = body.getInt(bodyStart + (int) offsets.offset + index * 4); + int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4); + Map map = new HashMap<>(); + for (int entry = start; entry < end; entry++) { + map.put(keys.get(entry), values.get(entry)); + } + return map.isEmpty() ? null : map; + } + } +} 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 cb859dfa595d..6bd5deb8cc55 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 @@ -47,6 +47,7 @@ import com.azure.storage.blob.models.PageBlobCopyIncrementalRequestConditions; import com.azure.storage.blob.models.PageRange; import com.azure.storage.blob.models.ParallelTransferOptions; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; import com.azure.storage.blob.models.TaggedBlobItem; import com.azure.storage.common.Utility; import com.azure.storage.common.implementation.Constants; @@ -85,6 +86,14 @@ public final class ModelHelper { */ public static final int PAGE_BYTES = 512; + /** + * The format that {@link StorageResponseSerializationFormat#AUTO} currently resolves to on the + * wire. Changing this constant is a behavioral change (and should be noted in the CHANGELOG) + * but it is not a public API change. + */ + private static final StorageResponseSerializationFormat DEFAULT_SERIALIZATION_FORMAT + = StorageResponseSerializationFormat.XML; + /** * Determines whether the passed authority is IP style, that is, it is of the format {@code :}. * @@ -663,6 +672,22 @@ public static BlobStorageException mapToBlobStorageException(BlobStorageExceptio internal.getResponse(), code, headerName), internal.getResponse(), internal.getValue()); } + /** + * Resolves a user-supplied {@link StorageResponseSerializationFormat} to the concrete value + * to send on the wire. Treats {@code null} and {@link StorageResponseSerializationFormat#AUTO} + * identically — both yield {@link #DEFAULT_SERIALIZATION_FORMAT}. + * + * @param format the format requested by the caller, or {@code null} if unset. + * @return the concrete {@link StorageResponseSerializationFormat} to send on the wire. + */ + public static StorageResponseSerializationFormat + resolveSerializationFormat(StorageResponseSerializationFormat format) { + if (format == null || format == StorageResponseSerializationFormat.AUTO) { + return DEFAULT_SERIALIZATION_FORMAT; + } + return format; + } + private ModelHelper() { } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java new file mode 100644 index 000000000000..44b985c4ddf6 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/BodyCompression.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code BodyCompression} table. + *

+ * The ListBlobs reader only needs to detect the presence of this table to reject compressed record batches, so no + * fields are exposed. + */ +public final class BodyCompression extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public BodyCompression __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java new file mode 100644 index 000000000000..9016b0595dc8 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Buffer.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Struct; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code Buffer} struct (offset/length of a buffer within a record batch body). + */ +public final class Buffer extends Struct { + /** + * Positions this accessor at the given struct offset. + * + * @param i the struct offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given struct offset. + * + * @param i the struct offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Buffer __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the byte offset of the buffer relative to the start of the record batch body. + * + * @return the buffer offset. + */ + public long offset() { + return bb.getLong(bb_pos); + } + + /** + * Gets the length, in bytes, of the buffer. + * + * @return the buffer length. + */ + public long length() { + return bb.getLong(bb_pos + 8); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java new file mode 100644 index 000000000000..af39bc7b6967 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Endianness.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +/** + * Values for the Arrow IPC {@code Endianness} enum. + */ +public final class Endianness { + private Endianness() { + } + + /** Little-endian byte order. */ + public static final short LITTLE = 0; + /** Big-endian byte order. */ + public static final short BIG = 1; +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java new file mode 100644 index 000000000000..5a27e0e52f24 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Field.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code Field} table describing a single column. + */ +public final class Field extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Field __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the field name. + * + * @return the field name, or {@code null} when absent. + */ + public String name() { + int o = __offset(4); + return o != 0 ? __string(o + bb_pos) : null; + } + + /** + * Gets the discriminator identifying the field's {@code type} union (see {@link Type}). + * + * @return the type union discriminator, or {@code 0} when absent. + */ + public byte typeType() { + int o = __offset(8); + return o != 0 ? bb.get(o + bb_pos) : 0; + } + + /** + * Resolves the {@code type} union value into the supplied accessor. + * + * @param obj the accessor to assign to the union value. + * @return the assigned accessor, or {@code null} when absent. + */ + public Table type(Table obj) { + int o = __offset(10); + return o != 0 ? __union(obj, o + bb_pos) : null; + } + + /** + * Gets the child field at the given index. + * + * @param j the child index. + * @return the child field accessor. + */ + public Field children(int j) { + return children(new Field(), j); + } + + /** + * Gets the child field at the given index into the supplied accessor. + * + * @param obj the accessor to assign. + * @param j the child index. + * @return the assigned accessor, or {@code null} when absent. + */ + public Field children(Field obj, int j) { + int o = __offset(14); + return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; + } + + /** + * Gets the number of child fields. + * + * @return the child field count. + */ + public int childrenLength() { + int o = __offset(14); + return o != 0 ? __vector_len(o) : 0; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java new file mode 100644 index 000000000000..2cf4f51fc732 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/FieldNode.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Struct; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code FieldNode} struct (per-column metadata within a record batch). + */ +public final class FieldNode extends Struct { + /** + * Positions this accessor at the given struct offset. + * + * @param i the struct offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given struct offset. + * + * @param i the struct offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public FieldNode __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the number of value slots in the column. + * + * @return the value count. + */ + public long length() { + return bb.getLong(bb_pos); + } + + /** + * Gets the number of null value slots in the column. + * + * @return the null count. + */ + public long nullCount() { + return bb.getLong(bb_pos + 8); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java new file mode 100644 index 000000000000..cef3b699d894 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Int.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code Int} type table. + */ +public final class Int extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Int __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the bit width of the integer (8, 16, 32, or 64). + * + * @return the bit width, or {@code 0} when absent. + */ + public int bitWidth() { + int o = __offset(4); + return o != 0 ? bb.getInt(o + bb_pos) : 0; + } + + /** + * Gets whether the integer is signed. + * + * @return {@code true} if signed, otherwise {@code false}. + */ + public boolean isSigned() { + int o = __offset(6); + return o != 0 && 0 != bb.get(o + bb_pos); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java new file mode 100644 index 000000000000..7b26bb99a59b --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/KeyValue.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code KeyValue} table (a single custom metadata entry). + */ +public final class KeyValue extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public KeyValue __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the metadata key. + * + * @return the key, or {@code null} when absent. + */ + public String key() { + int o = __offset(4); + return o != 0 ? __string(o + bb_pos) : null; + } + + /** + * Gets the metadata value. + * + * @return the value, or {@code null} when absent. + */ + public String value() { + int o = __offset(6); + return o != 0 ? __string(o + bb_pos) : null; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java new file mode 100644 index 000000000000..3fef56e061de --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Message.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Accessor for the Arrow IPC {@code Message} table (root of every encapsulated IPC message). + */ +public final class Message extends Table { + /** + * Reads the {@code Message} located at the root offset of the supplied buffer. + * + * @param bb the little-endian buffer positioned at the start of the message. + * @return the {@code Message} accessor. + */ + public static Message getRootAsMessage(ByteBuffer bb) { + return getRootAsMessage(bb, new Message()); + } + + /** + * Reads the {@code Message} located at the root offset of the supplied buffer into {@code obj}. + * + * @param bb the buffer positioned at the start of the message. + * @param obj the accessor instance to assign. + * @return the assigned {@code Message} accessor. + */ + public static Message getRootAsMessage(ByteBuffer bb, Message obj) { + bb.order(ByteOrder.LITTLE_ENDIAN); + // A FlatBuffer begins with a 4-byte offset (relative to here) pointing to the root table. + int rootTableOffset = bb.getInt(bb.position()) + bb.position(); + return obj.__assign(rootTableOffset, bb); + } + + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Message __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the discriminator identifying the type of the {@code header} union (see {@link MessageHeader}). + * + * @return the header union type, or {@code 0} when absent. + */ + public byte headerType() { + int o = __offset(6); + return o != 0 ? bb.get(o + bb_pos) : 0; + } + + /** + * Resolves the {@code header} union value into the supplied accessor. + * + * @param obj the accessor to assign to the union value. + * @return the assigned accessor, or {@code null} when the header is absent. + */ + public Table header(Table obj) { + int o = __offset(8); + return o != 0 ? __union(obj, o + bb_pos) : null; + } + + /** + * Gets the length, in bytes, of the message body that follows the metadata. + * + * @return the body length, or {@code 0} when absent. + */ + public long bodyLength() { + int o = __offset(10); + return o != 0 ? bb.getLong(o + bb_pos) : 0L; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java new file mode 100644 index 000000000000..8eca35389946 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/MessageHeader.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +/** + * Discriminator values for the Arrow IPC {@code MessageHeader} union. + *

+ * Only the subset that a ListBlobs response can contain is defined as active constants: {@link #SCHEMA} and + * {@link #RECORD_BATCH} (a ListBlobs payload is a tabular result, i.e. a schema followed by record batches), + * plus {@link #DICTIONARY_BATCH}, which the reader recognizes solely to reject it. The union's remaining members, + * {@code TENSOR} (4) and {@code SPARSE_TENSOR} (5), are valid in the Arrow format but are never emitted for a + * ListBlobs response, so they are intentionally kept commented out below rather than deleted. If a future service or + * format change ever starts sending them, uncomment those constants and add the corresponding handling in + * {@code BlobListArrowStreamReader}. + */ +public final class MessageHeader { + private MessageHeader() { + } + + /** No header. */ + public static final byte NONE = 0; + /** A {@link Schema} header. */ + public static final byte SCHEMA = 1; + /** A dictionary batch header. */ + public static final byte DICTIONARY_BATCH = 2; + /** A {@link RecordBatch} header. */ + public static final byte RECORD_BATCH = 3; + // TENSOR (4) and SPARSE_TENSOR (5) are omitted on purpose; see the class javadoc for why and how to revive them. + // public static final byte TENSOR = 4; + // public static final byte SPARSE_TENSOR = 5; +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java new file mode 100644 index 000000000000..63697054c91a --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/RecordBatch.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code RecordBatch} table. + */ +public final class RecordBatch extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public RecordBatch __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the number of rows in the record batch. + * + * @return the row count, or {@code 0} when absent. + */ + public long length() { + int o = __offset(4); + return o != 0 ? bb.getLong(o + bb_pos) : 0L; + } + + /** + * Gets the field node at the given index. + * + * @param j the node index. + * @return the field node accessor. + */ + public FieldNode nodes(int j) { + return nodes(new FieldNode(), j); + } + + /** + * Gets the field node at the given index into the supplied accessor. + * + * @param obj the accessor to assign. + * @param j the node index. + * @return the assigned accessor, or {@code null} when absent. + */ + public FieldNode nodes(FieldNode obj, int j) { + int o = __offset(6); + return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null; + } + + /** + * Gets the number of field nodes. + * + * @return the field node count. + */ + public int nodesLength() { + int o = __offset(6); + return o != 0 ? __vector_len(o) : 0; + } + + /** + * Gets the buffer region at the given index. + * + * @param j the buffer index. + * @return the buffer accessor. + */ + public Buffer buffers(int j) { + return buffers(new Buffer(), j); + } + + /** + * Gets the buffer region at the given index into the supplied accessor. + * + * @param obj the accessor to assign. + * @param j the buffer index. + * @return the assigned accessor, or {@code null} when absent. + */ + public Buffer buffers(Buffer obj, int j) { + int o = __offset(8); + return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null; + } + + /** + * Gets the number of buffers. + * + * @return the buffer count. + */ + public int buffersLength() { + int o = __offset(8); + return o != 0 ? __vector_len(o) : 0; + } + + /** + * Gets the optional body compression descriptor. + * + * @return the body compression accessor, or {@code null} when the batch is uncompressed. + */ + public BodyCompression compression() { + return compression(new BodyCompression()); + } + + /** + * Gets the optional body compression descriptor into the supplied accessor. + * + * @param obj the accessor to assign. + * @return the assigned accessor, or {@code null} when the batch is uncompressed. + */ + public BodyCompression compression(BodyCompression obj) { + int o = __offset(10); + return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java new file mode 100644 index 000000000000..1cf0f8fb0669 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Schema.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code Schema} table. + */ +public final class Schema extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Schema __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the byte order of the schema's buffers (see {@link Endianness}). + * + * @return the endianness, or {@code 0} ({@link Endianness#LITTLE}) when absent. + */ + public short endianness() { + int o = __offset(4); + return o != 0 ? bb.getShort(o + bb_pos) : 0; + } + + /** + * Gets the field at the given index. + * + * @param j the field index. + * @return the field accessor. + */ + public Field fields(int j) { + return fields(new Field(), j); + } + + /** + * Gets the field at the given index into the supplied accessor. + * + * @param obj the accessor to assign. + * @param j the field index. + * @return the assigned accessor, or {@code null} when absent. + */ + public Field fields(Field obj, int j) { + int o = __offset(6); + return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; + } + + /** + * Gets the number of top-level fields in the schema. + * + * @return the field count. + */ + public int fieldsLength() { + int o = __offset(6); + return o != 0 ? __vector_len(o) : 0; + } + + /** + * Gets the custom metadata entry at the given index. + * + * @param j the entry index. + * @return the key/value accessor. + */ + public KeyValue customMetadata(int j) { + return customMetadata(new KeyValue(), j); + } + + /** + * Gets the custom metadata entry at the given index into the supplied accessor. + * + * @param obj the accessor to assign. + * @param j the entry index. + * @return the assigned accessor, or {@code null} when absent. + */ + public KeyValue customMetadata(KeyValue obj, int j) { + int o = __offset(8); + return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; + } + + /** + * Gets the number of custom metadata entries. + * + * @return the entry count. + */ + public int customMetadataLength() { + int o = __offset(8); + return o != 0 ? __vector_len(o) : 0; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java new file mode 100644 index 000000000000..2ba6d3072d40 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/TimeUnit.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +/** + * Values for the Arrow IPC {@code TimeUnit} enum. + */ +public final class TimeUnit { + private TimeUnit() { + } + + /** Second resolution. */ + public static final short SECOND = 0; + /** Millisecond resolution. */ + public static final short MILLISECOND = 1; + /** Microsecond resolution. */ + public static final short MICROSECOND = 2; + /** Nanosecond resolution. */ + public static final short NANOSECOND = 3; + + private static final String[] NAMES = { "SECOND", "MILLISECOND", "MICROSECOND", "NANOSECOND" }; + + /** + * Gets the canonical Arrow name for a {@code TimeUnit} value, for diagnostic messages. + * + * @param e the time unit value. + * @return the canonical Arrow time unit name. + */ + public static String name(int e) { + return NAMES[e]; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java new file mode 100644 index 000000000000..e22e9b6147d6 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Timestamp.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; + +/** + * Accessor for the Arrow IPC {@code Timestamp} type table. + */ +public final class Timestamp extends Table { + /** + * Positions this accessor at the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + */ + public void __init(int i, ByteBuffer bb) { + __reset(i, bb); + } + + /** + * Assigns this accessor to the given table offset. + * + * @param i the table offset. + * @param bb the backing buffer. + * @return this accessor. + */ + public Timestamp __assign(int i, ByteBuffer bb) { + __init(i, bb); + return this; + } + + /** + * Gets the timestamp resolution (see {@link TimeUnit}). + * + * @return the time unit, or {@code 0} ({@link TimeUnit#SECOND}) when absent. + */ + public short unit() { + int o = __offset(4); + return o != 0 ? bb.getShort(o + bb_pos) : 0; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java new file mode 100644 index 000000000000..690eebf19562 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/Type.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* + * Portions Copyright (c) Microsoft Corporation + */ + +package com.azure.storage.blob.implementation.util.apachearrow; + +/** + * Discriminator values for the Arrow IPC {@code Type} union, identifying a field's logical type. + */ +public final class Type { + private Type() { + } + + /** No type. */ + public static final byte NONE = 0; + /** Null type. */ + public static final byte NULL = 1; + /** Integer type (see {@link Int}). */ + public static final byte INT = 2; + /** Floating-point type. */ + public static final byte FLOATING_POINT = 3; + /** Variable-length binary type. */ + public static final byte BINARY = 4; + /** Variable-length UTF-8 string type. */ + public static final byte UTF8 = 5; + /** Boolean type. */ + public static final byte BOOL = 6; + /** Decimal type. */ + public static final byte DECIMAL = 7; + /** Date type. */ + public static final byte DATE = 8; + /** Time-of-day type. */ + public static final byte TIME = 9; + /** Timestamp type (see {@link Timestamp}). */ + public static final byte TIMESTAMP = 10; + /** Interval type. */ + public static final byte INTERVAL = 11; + /** List type. */ + public static final byte LIST = 12; + /** Struct type. */ + public static final byte STRUCT = 13; + /** Union type. */ + public static final byte UNION = 14; + /** Fixed-size binary type. */ + public static final byte FIXED_SIZE_BINARY = 15; + /** Fixed-size list type. */ + public static final byte FIXED_SIZE_LIST = 16; + /** Map type. */ + public static final byte MAP = 17; + /** Duration type. */ + public static final byte DURATION = 18; + /** Large variable-length binary type. */ + public static final byte LARGE_BINARY = 19; + /** Large variable-length UTF-8 string type. */ + public static final byte LARGE_UTF8 = 20; + /** Large list type. */ + public static final byte LARGE_LIST = 21; + /** Run-end encoded type. */ + public static final byte RUN_END_ENCODED = 22; + /** Binary view type. */ + public static final byte BINARY_VIEW = 23; + /** UTF-8 string view type. */ + public static final byte UTF8_VIEW = 24; + /** List view type. */ + public static final byte LIST_VIEW = 25; + /** Large list view type. */ + public static final byte LARGE_LIST_VIEW = 26; + + private static final String[] NAMES = { + "NONE", + "Null", + "Int", + "FloatingPoint", + "Binary", + "Utf8", + "Bool", + "Decimal", + "Date", + "Time", + "Timestamp", + "Interval", + "List", + "Struct_", + "Union", + "FixedSizeBinary", + "FixedSizeList", + "Map", + "Duration", + "LargeBinary", + "LargeUtf8", + "LargeList", + "RunEndEncoded", + "BinaryView", + "Utf8View", + "ListView", + "LargeListView" }; + + /** + * Gets the canonical Arrow name for a {@code Type} union discriminator, for diagnostic messages. + * + * @param e the discriminator value. + * @return the canonical Arrow type name. + */ + public static String name(int e) { + return NAMES[e]; + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java new file mode 100644 index 000000000000..dff44c7d8543 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/apachearrow/package-info.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Minimal Apache Arrow IPC FlatBuffer metadata accessors used internally by the Blob Storage ListBlobs Arrow reader. + *

+ * The classes in this package are thin {@link com.google.flatbuffers.Table}/{@link com.google.flatbuffers.Struct} + * accessors over the FlatBuffer-encoded metadata of the Apache Arrow IPC format. They expose only the small subset of + * the {@code org.apache.arrow.flatbuf} schema that {@code BlobListArrowStreamReader} requires, so that the main + * (Java 8 baseline) compile classpath does not depend on the {@code arrow-format} artifact, which ships Java 11 + * bytecode. + *

+ * The field orderings (FlatBuffer vtable slots) and enum values are defined by the public Apache Arrow columnar format + * specification (see {@code Schema.fbs} and {@code Message.fbs} in the Apache Arrow project, licensed under the Apache + * License, Version 2.0) and must match the on-the-wire layout produced by the Storage service. + */ +package com.azure.storage.blob.implementation.util.apachearrow; diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java index 6a372f5b2c74..dfd93a535fc6 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java @@ -825,97 +825,6 @@ public BlobDownloadHeaders setEncryptionScope(String encryptionScope) { return this; } - /** - * Gets the access tier of the blob. - * - * @return the access tier of the blob. This is only set for Page blobs on a premium storage account or for Block - * blobs on blob storage or general purpose V2 account. - */ - public AccessTier getAccessTier() { - String accessTier = internalHeaders.getXMsAccessTier(); - return accessTier == null ? null : AccessTier.fromString(accessTier); - } - - /** - * Sets the access tier of the blob. - * - * @param accessTier the access tier of the blob. - * @return the BlobDownloadHeaders object itself. - */ - public BlobDownloadHeaders setAccessTier(AccessTier accessTier) { - internalHeaders.setXMsAccessTier(accessTier == null ? null : accessTier.toString()); - return this; - } - - /** - * Gets the status of the tier being inferred for the blob. - * - * @return the status of the tier being inferred for the blob. This is only set for Page blobs on a premium storage - * account or for Block blobs on blob storage or general purpose V2 account. - */ - public Boolean isAccessTierInferred() { - return Boolean.TRUE.equals(internalHeaders.isXMsAccessTierInferred()); - } - - /** - * Sets the status of the tier being inferred for the blob. - * - * @param accessTierInferred the status of the tier being inferred for the blob. - * @return the BlobDownloadHeaders object itself. - */ - public BlobDownloadHeaders setAccessTierInferred(Boolean accessTierInferred) { - internalHeaders.setXMsAccessTierInferred(accessTierInferred); - return this; - } - - /** - * Gets the time when the access tier for the blob was last changed. - * - * @return the time when the access tier for the blob was last changed. - */ - public OffsetDateTime getAccessTierChangeTime() { - return internalHeaders.getXMsAccessTierChangeTime(); - } - - /** - * Sets the time when the access tier for the blob was last changed. - * - * @param accessTierChangeTime the time when the access tier for the blob was last changed. - * @return the BlobDownloadHeaders object itself. - */ - public BlobDownloadHeaders setAccessTierChangeTime(OffsetDateTime accessTierChangeTime) { - internalHeaders.setXMsAccessTierChangeTime(accessTierChangeTime); - return this; - } - - /** - * Gets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}. - *

- * This value is only populated when {@link #getAccessTier()} returns {@link AccessTier#SMART}. In that case, it - * represents the concrete access tier (for example {@link AccessTier#HOT} or {@link AccessTier#COOL}) that the - * service has selected for the blob. For all other access tiers, this property is {@code null} and should be - * ignored. - * - * @return the underlying access tier chosen by the service when the blob's access tier is {@link AccessTier#SMART}, - * or {@code null} if the blob is not using the smart access tier. - */ - public AccessTier getSmartAccessTier() { - String smartAccessTier = internalHeaders.getXMsSmartAccessTier(); - return smartAccessTier == null ? null : AccessTier.fromString(smartAccessTier); - } - - /** - * Sets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}. - * - * @param smartAccessTier the underlying access tier chosen by the service when the blob's access tier is - * {@link AccessTier#SMART}. - * @return the BlobDownloadHeaders object itself. - */ - public BlobDownloadHeaders setSmartAccessTier(AccessTier smartAccessTier) { - internalHeaders.setXMsSmartAccessTier(smartAccessTier == null ? null : smartAccessTier.toString()); - return this; - } - /** * Get the blobContentMD5 property: If the blob has a MD5 hash, and if request contains range header (Range or * x-ms-range), this response header is returned with the value of the whole blob's MD5 value. This value may or may diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java index 47f29c2ab2b3..e434681ac6e9 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java @@ -19,6 +19,8 @@ public final class ListBlobsOptions { private String prefix; private String startFrom; private Integer maxResultsPerPage; + private StorageResponseSerializationFormat storageResponseSerializationFormat; + private String endBefore; /** * Constructs an unpopulated {@link ListBlobsOptions}. @@ -74,7 +76,7 @@ public ListBlobsOptions setPrefix(String prefix) { * This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container. * For non-recursive lists, only one entity level is supported. * - * @return the marker indicating where to start listing blobs + * @return the marker indicating where to start listing blobs (inclusive) */ public String getStartFrom() { return startFrom; @@ -84,7 +86,7 @@ public String getStartFrom() { * Sets an optional parameter that specifies an absolute path within the container. This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container. * For non-recursive lists, only one entity level is supported. * - * @param startFrom The marker indicating where to start listing blobs + * @param startFrom The marker indicating where to start listing blobs (inclusive) * @return the updated ListBlobsOptions object */ public ListBlobsOptions setStartFrom(String startFrom) { @@ -92,6 +94,49 @@ public ListBlobsOptions setStartFrom(String startFrom) { return this; } + /** + * Gets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive). + * + * @return the endBefore value. + */ + public String getEndBefore() { + return endBefore; + } + + /** + * Sets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive). + * + * @param endBefore the endBefore value to set. + * @return the updated ListBlobsOptions object. + */ + public ListBlobsOptions setEndBefore(String endBefore) { + this.endBefore = endBefore; + return this; + } + + /** + * Gets the response serialization format the service should use when listing blobs. + * + * @return the {@link StorageResponseSerializationFormat}, or {@code null} if unset + * (equivalent to {@link StorageResponseSerializationFormat#AUTO}). + */ + public StorageResponseSerializationFormat getStorageResponseSerializationFormat() { + return storageResponseSerializationFormat; + } + + /** + * Sets the response serialization format the service should use when listing blobs. + * + * @param storageResponseSerializationFormat the format to request. {@code null} and + * {@link StorageResponseSerializationFormat#AUTO} both let the SDK pick. + * @return the updated {@link ListBlobsOptions} object. + */ + public ListBlobsOptions + setStorageResponseSerializationFormat(StorageResponseSerializationFormat storageResponseSerializationFormat) { + this.storageResponseSerializationFormat = storageResponseSerializationFormat; + return this; + } + /** * Specifies the maximum number of blobs to return, including all BlobPrefix elements. If the request does not * specify maxResultsPerPage or specifies a value greater than 5,000, the server will return up to 5,000 items. diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java new file mode 100644 index 000000000000..e5017accedbc --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.models; + +/** + * Defines the serialization format the service uses for list-blobs responses. + */ +public enum StorageResponseSerializationFormat { + /** + * Let the SDK choose the serialization format that is most appropriate for the request. + *

+ * The exact format selected by {@code AUTO} is an implementation detail and may change + * between SDK releases. Choose {@link #XML} or {@link #ARROW} explicitly if you require + * a specific format. + */ + AUTO, + + /** + * XML response format. + */ + XML, + + /** + * Apache Arrow response format. + */ + ARROW +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/module-info.java b/sdk/storage/azure-storage-blob/src/main/java/module-info.java index 597a417add7f..e6947c9af0c4 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/module-info.java +++ b/sdk/storage/azure-storage-blob/src/main/java/module-info.java @@ -5,6 +5,7 @@ requires transitive com.azure.storage.common; requires com.azure.storage.internal.avro; + requires flatbuffers.java; exports com.azure.storage.blob; exports com.azure.storage.blob.models; diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java index 71c474ba295c..50a9eb63ef21 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java @@ -543,36 +543,6 @@ public void downloadAllNullBinaryData() { // headers.getLastAccessedTime() /* TODO (gapra): re-enable when last access time enabled. */ } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") - @Test - public void downloadSmartAccessTierHeaders() { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bc.setAccessTier(AccessTier.SMART); - - BlobDownloadResponse response = bc.downloadStreamWithResponse(stream, null, null, null, false, null, null); - ByteBuffer body = ByteBuffer.wrap(stream.toByteArray()); - - assertEquals(DATA.getDefaultData(), body); - assertSmartAccessTierHeaders(response.getDeserializedHeaders()); - } - - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") - @Test - public void downloadContentSmartAccessTierHeaders() { - bc.setAccessTier(AccessTier.SMART); - BlobDownloadContentResponse response = bc.downloadContentWithResponse(null, null, null, null); - - TestUtils.assertArraysEqual(DATA.getDefaultBytes(), response.getValue().toBytes()); - assertSmartAccessTierHeaders(response.getDeserializedHeaders()); - } - - private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) { - assertEquals(AccessTier.SMART, headers.getAccessTier()); - assertNotNull(headers.getSmartAccessTier()); - assertFalse(headers.isAccessTierInferred()); - assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime()); - } - @Test public void downloadEmptyFile() { AppendBlobClient bc = cc.getBlobClient("emptyAppendBlob").getAppendBlobClient(); diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java index ea01df338d18..049e4254e92a 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java @@ -382,33 +382,6 @@ public void downloadAllNullBinaryData() { .verifyComplete(); } - @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06") - @Test - public void downloadSmartAccessTierHeaders() { - Mono response = bc.setAccessTier(AccessTier.SMART) - .then(bc.downloadStreamWithResponse(null, null, null, false)) - .flatMap(r -> { - assertSmartAccessTierHeaders(r.getDeserializedHeaders()); - return FluxUtil.collectBytesInByteBufferStream(r.getValue()); - }) - .flatMap(r -> { - TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r); - return bc.downloadContentWithResponse(null, null); - }); - - StepVerifier.create(response).assertNext(r -> { - assertSmartAccessTierHeaders(r.getDeserializedHeaders()); - TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r.getValue().toBytes()); - }).verifyComplete(); - } - - private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) { - assertEquals(AccessTier.SMART, headers.getAccessTier()); - assertNotNull(headers.getSmartAccessTier()); - assertFalse(headers.isAccessTierInferred()); - assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime()); - } - @Test public void downloadEmptyFile() { AppendBlobAsyncClient bc = ccAsync.getBlobAsyncClient("emptyAppendBlob").getAppendBlobAsyncClient(); 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 f46116acdbb5..4eea5979e577 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 @@ -34,6 +34,7 @@ import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.RehydratePriority; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.StorageResponseSerializationFormat; import com.azure.storage.blob.models.TaggedBlobItem; import com.azure.storage.blob.options.BlobContainerCreateOptions; import com.azure.storage.blob.options.BlobParallelUploadOptions; @@ -45,6 +46,7 @@ import com.azure.storage.blob.specialized.PageBlobClient; import com.azure.storage.common.Utility; import com.azure.storage.common.implementation.Constants; +import com.azure.storage.common.implementation.StorageImplUtils; import com.azure.storage.common.test.shared.TestHttpClientType; import com.azure.storage.common.test.shared.extensions.LiveOnly; import com.azure.storage.common.test.shared.extensions.PlaybackOnly; @@ -58,7 +60,17 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import com.azure.storage.blob.implementation.AzureBlobStorageImpl; +import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder; +import com.azure.storage.blob.implementation.models.ContainersListBlobFlatSegmentApacheArrowHeaders; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer; +import com.azure.storage.blob.implementation.util.ModelHelper; +import com.azure.storage.blob.models.ListBlobsIncludeItem; +import com.azure.core.http.rest.ResponseBase; + import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; import java.net.URL; import java.time.OffsetDateTime; import java.util.Arrays; @@ -2128,4 +2140,315 @@ public void getBlobContainerUrlEncodesContainerName() { // then: // assertThrows(BlobStorageException.class, () -> // } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowBasic() { + // Upload a test blob + String blobName = generateBlobName(); + cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList()); + + assertEquals(1, blobs.size()); + assertEquals(blobName, blobs.get(0).getName()); + assertNotNull(blobs.get(0).getProperties()); + assertEquals(DATA.getDefaultDataSize(), blobs.get(0).getProperties().getContentLength()); + assertEquals(BlobType.BLOCK_BLOB, blobs.get(0).getProperties().getBlobType()); + assertNotNull(blobs.get(0).getProperties().getLastModified()); + assertNotNull(blobs.get(0).getProperties().getETag()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowWithTags() { + // Upload a blob and set tags + String blobName = generateBlobName(); + cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + Map tags = new HashMap<>(); + tags.put("tagkey", "tagvalue"); + cc.getBlobClient(blobName).setTags(tags); + + // List with Arrow + retrieveTags + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setDetails(new BlobListDetails().setRetrieveTags(true)); + List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList()); + + assertEquals(1, blobs.size()); + assertEquals(blobName, blobs.get(0).getName()); + assertNotNull(blobs.get(0).getTags()); + assertEquals("tagvalue", blobs.get(0).getTags().get("tagkey")); + } + + @ParameterizedTest + @MethodSource("listBlobsFlatRehydratePrioritySupplier") + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) { + String name = generateBlobName(); + BlockBlobClient bc = cc.getBlobClient(name).getBlockBlobClient(); + + bc.upload(DATA.getDefaultInputStream(), 7); + + if (rehydratePriority != null) { + bc.setAccessTier(AccessTier.ARCHIVE); + bc.setAccessTierWithResponse(new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority), + null, null); + } + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + BlobItem item = cc.listBlobs(options, null).iterator().next(); + + assertEquals(rehydratePriority, item.getProperties().getRehydratePriority()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowWithMetadata() { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + cc.getBlobClient(blobName) + .getBlockBlobClient() + .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null, + null, null, null); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setDetails(new BlobListDetails().setRetrieveMetadata(true)); + List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList()); + + assertEquals(1, blobs.size()); + assertNotNull(blobs.get(0).getMetadata()); + assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey")); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowPagination() { + // Upload 3 blobs + for (int i = 0; i < 4; i++) { + cc.getBlobClient("blob" + i) + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + } + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setMaxResultsPerPage(1); + List allBlobs = new ArrayList<>(); + for (PagedResponse page : cc.listBlobs(options, null).iterableByPage()) { + assertTrue(page.getValue().size() <= 1); + allBlobs.addAll(page.getValue()); + } + + cc.listBlobs().iterableByPage(2).forEach(page -> { + assertEquals(2, page.getValue().size()); + }); + + assertEquals(4, allBlobs.size()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowNullUseArrowUsesXml() { + // Default apacheArrowEnabled is null — should use XML path without error + String blobName = generateBlobName(); + cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options = new ListBlobsOptions(); + assertNull(options.getStorageResponseSerializationFormat()); + + List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList()); + assertEquals(1, blobs.size()); + assertEquals(blobName, blobs.get(0).getName()); + } + + @LiveOnly + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowEncryptedBlob() { + // Upload a blob with CPK (customer-provided key) + String blobName = generateBlobName(); + CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey())); + BlobClient cpkClient = cc.getBlobClient(blobName).getCustomerProvidedKeyClient(cpk); + cpkClient.getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList()); + + assertEquals(1, blobs.size()); + assertEquals(blobName, blobs.get(0).getName()); + // CPK blob should have server-encrypted = true + assertTrue(blobs.get(0).getProperties().isServerEncrypted()); + // Metadata should be null (no metadata was set) + assertNull(blobs.get(0).getMetadata()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowDeserializer() throws Exception { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + cc.getBlobClient(blobName) + .getBlockBlobClient() + .uploadWithResponse(DATA.getDefaultInputStream(), 7, null, metadata, null, null, null, null, null); + + AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(cc.getHttpPipeline()) + .url(cc.getAccountUrl()) + .version(BlobServiceVersion.getLatest().getVersion()) + .buildClient(); + + // Call the Arrow endpoint + ArrayList include = new ArrayList<>(); + include.add(ListBlobsIncludeItem.METADATA); + + ResponseBase response = impl.getContainers() + .listBlobFlatSegmentApacheArrowWithResponse(containerName, null, null, null, include, null, null, null, + null, com.azure.core.util.Context.NONE); + + // Verify Content-Type is Arrow + String contentType = response.getDeserializedHeaders().getContentType(); + assertTrue( + StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM), + "Expected Arrow content type but got: " + contentType); + + // Deserialize using ArrowBlobListDeserializer + ArrowBlobListDeserializer.ArrowListBlobsResult result + = ArrowBlobListDeserializer.deserialize(response.getValue()); + + // Verify pagination — single blob, no next page + assertNull(result.getNextMarker()); + + // Verify we got exactly one blob + assertEquals(1, result.getBlobItems().size()); + + com.azure.storage.blob.implementation.models.BlobItemInternal item = result.getBlobItems().get(0); + + // Name + assertNotNull(item.getName()); + assertEquals(blobName, item.getName().getContent()); + + // Properties + assertNotNull(item.getProperties()); + assertEquals(7L, (long) item.getProperties().getContentLength()); + assertEquals("application/octet-stream", item.getProperties().getContentType()); + assertNotNull(item.getProperties().getETag()); + assertNotNull(item.getProperties().getLastModified()); + assertNotNull(item.getProperties().getCreationTime()); + assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType()); + assertEquals(AccessTier.HOT, item.getProperties().getAccessTier()); + assertTrue(item.getProperties().isAccessTierInferred()); + assertTrue(item.getProperties().isServerEncrypted()); + assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState()); + assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus()); + assertNotNull(item.getProperties().getContentMd5()); + + // Metadata + assertNotNull(item.getMetadata()); + assertEquals("testvalue", item.getMetadata().get("testkey")); + + // Verify ModelHelper can convert to public BlobItem + BlobItem publicItem = ModelHelper.populateBlobItem(item); + assertEquals(blobName, publicItem.getName()); + assertEquals(7L, (long) publicItem.getProperties().getContentLength()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowBasic() { + // Upload blobs in a directory structure + cc.getBlobClient("dir/blob1") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + cc.getBlobClient("dir/blob2") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + cc.getBlobClient("topblob") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + List items = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList()); + + // Root level: one prefix "dir/" and one blob "topblob" + assertEquals(2, items.size()); + + BlobItem prefixItem = items.stream().filter(BlobItem::isPrefix).findFirst().orElse(null); + BlobItem blobItem = items.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null); + + assertNotNull(prefixItem); + assertEquals("dir/", prefixItem.getName()); + assertTrue(prefixItem.isPrefix()); + + assertNotNull(blobItem); + assertEquals("topblob", blobItem.getName()); + assertFalse(blobItem.isPrefix()); + assertNotNull(blobItem.getProperties()); + assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength()); + assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType()); + assertNotNull(blobItem.getProperties().getLastModified()); + assertNotNull(blobItem.getProperties().getETag()); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowWithMetadata() { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + cc.getBlobClient("dir/" + blobName) + .getBlockBlobClient() + .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null, + null, null, null); + cc.getBlobClient("topblob") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setPrefix("dir/") + .setDetails(new BlobListDetails().setRetrieveMetadata(true)); + List blobs = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList()); + + assertEquals(1, blobs.size()); + assertFalse(blobs.get(0).isPrefix()); + assertNotNull(blobs.get(0).getMetadata()); + assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey")); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowPagination() { + // Upload blobs across multiple directories + for (int i = 0; i < 3; i++) { + cc.getBlobClient("dir" + i + "/blob") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + } + cc.getBlobClient("topblob") + .getBlockBlobClient() + .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setMaxResultsPerPage(1); + List allItems = new ArrayList<>(); + for (PagedResponse page : cc.listBlobsByHierarchy("/", options, null).iterableByPage()) { + assertTrue(page.getValue().size() <= 1); + allItems.addAll(page.getValue()); + } + + // 3 prefixes + 1 blob = 4 items + assertEquals(4, allItems.size()); + } + } 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 04ebc06dc2b6..f0b87b33ccd2 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 @@ -10,8 +10,14 @@ import com.azure.core.test.TestMode; import com.azure.core.test.utils.MockTokenCredential; import com.azure.core.util.Context; +import com.azure.core.util.FluxUtil; import com.azure.core.util.polling.PollerFlux; import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.implementation.AzureBlobStorageImpl; +import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder; +import com.azure.storage.blob.implementation.models.BlobItemInternal; +import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer; +import com.azure.storage.blob.implementation.util.ModelHelper; import com.azure.storage.blob.models.*; import com.azure.storage.blob.options.BlobContainerCreateOptions; import com.azure.storage.blob.options.BlobParallelUploadOptions; @@ -22,6 +28,8 @@ import com.azure.storage.blob.specialized.BlobAsyncClientBase; import com.azure.storage.blob.specialized.BlockBlobAsyncClient; import com.azure.storage.blob.specialized.PageBlobAsyncClient; +import com.azure.storage.common.implementation.Constants; +import com.azure.storage.common.implementation.StorageImplUtils; import com.azure.storage.common.test.shared.TestHttpClientType; import com.azure.storage.common.test.shared.extensions.LiveOnly; import com.azure.storage.common.test.shared.extensions.PlaybackOnly; @@ -41,6 +49,7 @@ import reactor.util.function.Tuple2; import java.net.URL; +import java.io.ByteArrayInputStream; import java.time.Duration; import java.time.OffsetDateTime; import java.util.*; @@ -2142,4 +2151,336 @@ public void getBlobContainerUrlEncodesContainerName() { assertTrue(containerClient.getBlobContainerUrl().contains("my%20container")); } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowBasic() { + // Upload a test blob + String blobName = generateBlobName(); + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient(); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + + StepVerifier + .create( + bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null))) + .assertNext(item -> { + assertEquals(blobName, item.getName()); + assertNotNull(item.getProperties()); + assertEquals(DATA.getDefaultDataSize(), item.getProperties().getContentLength()); + assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType()); + assertNotNull(item.getProperties().getLastModified()); + assertNotNull(item.getProperties().getETag()); + }) + .verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowWithMetadata() { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient(); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setDetails(new BlobListDetails().setRetrieveMetadata(true)); + + StepVerifier.create( + bc.uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null) + .thenMany(ccAsync.listBlobs(options, null))) + .assertNext(item -> { + assertNotNull(item.getMetadata()); + assertEquals("testvalue", item.getMetadata().get("testkey")); + }) + .verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowPagination() { + // Upload 4 blobs + Flux uploads = Flux.range(0, 4) + .flatMap(i -> ccAsync.getBlobAsyncClient("blob" + i) + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setMaxResultsPerPage(1); + + Mono> result = uploads.then(ccAsync.listBlobs(options, null).byPage().doOnNext(page -> { + assertTrue(page.getValue().size() <= 1); + }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList()); + + StepVerifier.create(result).assertNext(allBlobs -> assertEquals(4, allBlobs.size())).verifyComplete(); + + // Mirror the sync test's secondary assertion: requesting page size 2 yields exactly 2 blobs per page. + StepVerifier.create(ccAsync.listBlobs().byPage(2)).thenConsumeWhile(page -> { + assertEquals(2, page.getValue().size()); + return true; + }).verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowNullUseArrowUsesXml() { + // Default apacheArrowEnabled is null — should use XML path without error + String blobName = generateBlobName(); + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient(); + + ListBlobsOptions options = new ListBlobsOptions(); + assertNull(options.getStorageResponseSerializationFormat()); + + StepVerifier + .create( + bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null))) + .assertNext(item -> assertEquals(blobName, item.getName())) + .verifyComplete(); + } + + @LiveOnly + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowEncryptedBlob() { + // Upload a blob with CPK (customer-provided key) + String blobName = generateBlobName(); + CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey())); + BlockBlobAsyncClient cpkClient + = ccAsync.getBlobAsyncClient(blobName).getCustomerProvidedKeyAsyncClient(cpk).getBlockBlobAsyncClient(); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + + StepVerifier.create(cpkClient.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()) + .thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> { + assertEquals(blobName, item.getName()); + // CPK blob should have server-encrypted = true + assertTrue(item.getProperties().isServerEncrypted()); + // Metadata should be null (no metadata was set) + assertNull(item.getMetadata()); + }).verifyComplete(); + } + + @ParameterizedTest + @MethodSource("listBlobsFlatRehydratePrioritySupplier") + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) { + String name = generateBlobName(); + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(name).getBlockBlobAsyncClient(); + + Mono> rehydrate = Mono.empty(); + + if (rehydratePriority != null) { + rehydrate = bc.setAccessTier(AccessTier.ARCHIVE) + .then(bc.setAccessTierWithResponse( + new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority))); + } + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + + Flux response = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()) + .then(rehydrate) + .thenMany(ccAsync.listBlobs(options, null)); + + StepVerifier.create(response) + .assertNext(r -> assertEquals(rehydratePriority, r.getProperties().getRehydratePriority())) + .verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowDeserializer() { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient(); + + AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(ccAsync.getHttpPipeline()) + .url(ccAsync.getAccountUrl()) + .version(BlobServiceVersion.getLatest().getVersion()) + .buildClient(); + + List include = new ArrayList<>(); + include.add(ListBlobsIncludeItem.METADATA); + + Mono testMono + = bc.uploadWithResponse(DATA.getDefaultFlux(), 7, null, metadata, null, null, null) + .then(impl.getContainers() + .listBlobFlatSegmentApacheArrowWithResponseAsync(containerName, null, null, null, include, null, + null, null, null)) + .flatMap(response -> { + // Verify Content-Type is Arrow + String contentType = response.getDeserializedHeaders().getContentType(); + assertTrue( + StorageImplUtils.hasMatchingHeaderValue(contentType, + Constants.ContentTypeConstants.APPLICATION_VND_APACHE_ARROW_STREAM), + "Expected Arrow content type but got: " + contentType); + + // Collect the Flux body into a byte[] and feed it to the deserializer. + return FluxUtil.collectBytesInByteBufferStream(response.getValue()) + .map(bytes -> ArrowBlobListDeserializer.deserialize(new ByteArrayInputStream(bytes))); + }); + + StepVerifier.create(testMono).assertNext(result -> { + // Verify pagination — single blob, no next page + assertNull(result.getNextMarker()); + + // Verify we got exactly one blob + assertEquals(1, result.getBlobItems().size()); + + BlobItemInternal item = result.getBlobItems().get(0); + + // Name + assertNotNull(item.getName()); + assertEquals(blobName, item.getName().getContent()); + + // Properties + assertNotNull(item.getProperties()); + assertEquals(7L, (long) item.getProperties().getContentLength()); + assertEquals("application/octet-stream", item.getProperties().getContentType()); + assertNotNull(item.getProperties().getETag()); + assertNotNull(item.getProperties().getLastModified()); + assertNotNull(item.getProperties().getCreationTime()); + assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType()); + assertEquals(AccessTier.HOT, item.getProperties().getAccessTier()); + assertTrue(item.getProperties().isAccessTierInferred()); + assertTrue(item.getProperties().isServerEncrypted()); + assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState()); + assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus()); + assertNotNull(item.getProperties().getContentMd5()); + + // Metadata + assertNotNull(item.getMetadata()); + assertEquals("testvalue", item.getMetadata().get("testkey")); + + // Verify ModelHelper can convert to public BlobItem + BlobItem publicItem = ModelHelper.populateBlobItem(item); + assertEquals(blobName, publicItem.getName()); + assertEquals(7L, (long) publicItem.getProperties().getContentLength()); + }).verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowBasic() { + // Upload blobs in a directory structure + Flux uploads = Flux.concat( + ccAsync.getBlobAsyncClient("dir/blob1") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()), + ccAsync.getBlobAsyncClient("dir/blob2") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()), + ccAsync.getBlobAsyncClient("topblob") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW); + + Mono> items + = uploads.then(ccAsync.listBlobsByHierarchy("/", options).collect(Collectors.toList())); + + StepVerifier.create(items).assertNext(list -> { + // Root level: one prefix "dir/" and one blob "topblob" + assertEquals(2, list.size()); + + BlobItem prefixItem = list.stream().filter(BlobItem::isPrefix).findFirst().orElse(null); + BlobItem blobItem = list.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null); + + assertNotNull(prefixItem); + assertEquals("dir/", prefixItem.getName()); + assertTrue(prefixItem.isPrefix()); + + assertNotNull(blobItem); + assertEquals("topblob", blobItem.getName()); + assertFalse(blobItem.isPrefix()); + assertNotNull(blobItem.getProperties()); + assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength()); + assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType()); + assertNotNull(blobItem.getProperties().getLastModified()); + assertNotNull(blobItem.getProperties().getETag()); + }).verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowWithMetadata() { + String blobName = generateBlobName(); + Map metadata = new HashMap<>(); + metadata.put("testkey", "testvalue"); + + Mono uploads = ccAsync.getBlobAsyncClient("dir/" + blobName) + .getBlockBlobAsyncClient() + .uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null) + .then(ccAsync.getBlobAsyncClient("topblob") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setPrefix("dir/") + .setDetails(new BlobListDetails().setRetrieveMetadata(true)); + + StepVerifier.create(uploads.thenMany(ccAsync.listBlobsByHierarchy("/", options))).assertNext(item -> { + assertFalse(item.isPrefix()); + assertNotNull(item.getMetadata()); + assertEquals("testvalue", item.getMetadata().get("testkey")); + }).verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsByHierarchyArrowPagination() { + // Upload blobs across multiple directories + Flux uploads = Flux.concat( + Flux.range(0, 3) + .flatMap(i -> ccAsync.getBlobAsyncClient("dir" + i + "/blob") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())), + ccAsync.getBlobAsyncClient("topblob") + .getBlockBlobAsyncClient() + .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setMaxResultsPerPage(1); + + Mono> result + = uploads.then(ccAsync.listBlobsByHierarchy("/", options).byPage().doOnNext(page -> { + assertTrue(page.getValue().size() <= 1); + }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList()); + + // 3 prefixes + 1 blob = 4 items + StepVerifier.create(result).assertNext(allItems -> assertEquals(4, allItems.size())).verifyComplete(); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void listBlobsArrowWithTags() { + // Upload a blob and set tags + String blobName = generateBlobName(); + BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient(); + + Map tags = new HashMap<>(); + tags.put("tagkey", "tagvalue"); + + ListBlobsOptions options + = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW) + .setDetails(new BlobListDetails().setRetrieveTags(true)); + + Mono upload = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()) + .then(ccAsync.getBlobAsyncClient(blobName).setTags(tags)); + + StepVerifier.create(upload.thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> { + assertEquals(blobName, item.getName()); + assertNotNull(item.getTags()); + assertEquals("tagvalue", item.getTags().get("tagkey")); + }).verifyComplete(); + } } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java new file mode 100644 index 000000000000..ae5cedda007d --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowGoldenDecodeTests.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.storage.blob.implementation.models.BlobItemInternal; +import com.azure.storage.blob.implementation.models.BlobListArrowParseException; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Golden-payload decode tests for the vendored {@link BlobListArrowStreamReader} / {@link ArrowBlobListDeserializer}. + *

+ * The fixture ({@code arrow/representative.arrow.base64}) is a real Arrow IPC stream that was produced once by the + * official {@code arrow-vector} writer (see {@code ArrowFixtureGenerator}) and committed as a frozen resource. This test + * decodes those exact bytes with the vendored reader and pins every field the ListBlobs path relies on, so any + * regression in the vendored FlatBuffer accessors surfaces here without requiring the Arrow library on the test + * classpath. It replaces the former differential parity test that re-decoded a freshly written payload with Apache + * Arrow's own accessors; the frozen fixture is the same shape but removes the runtime dependency. + */ +public class BlobListArrowGoldenDecodeTests { + + @Test + public void decodesRepresentativePayload() throws IOException { + ArrowBlobListDeserializer.ArrowListBlobsResult result + = ArrowBlobListDeserializer.deserialize(openFixture("representative.arrow.base64")); + + // Schema metadata. + assertEquals("nextPage", result.getNextMarker()); + assertEquals(Integer.valueOf(2), result.getNumberOfRecords()); + + // Two rows: one blob, one prefix. + List items = result.getBlobItems(); + assertEquals(2, items.size()); + + BlobItemInternal blob = items.get(0); + assertNotNull(blob.getName()); + assertEquals("blob1", blob.getName().getContent()); + assertNull(blob.isPrefix()); + assertEquals(Boolean.FALSE, blob.isDeleted()); + assertNotNull(blob.getProperties()); + assertEquals(7L, (long) blob.getProperties().getContentLength()); + assertEquals("application/octet-stream", blob.getProperties().getContentType()); + assertNotNull(blob.getProperties().getCreationTime()); + assertEquals(1000L, blob.getProperties().getCreationTime().toEpochSecond()); + + Map metadata = blob.getMetadata(); + assertNotNull(metadata); + assertEquals("v1", metadata.get("k1")); + assertEquals("v2", metadata.get("k2")); + + BlobItemInternal prefix = items.get(1); + assertNotNull(prefix.getName()); + assertEquals("dir/", prefix.getName().getContent()); + assertTrue(prefix.isPrefix()); + // The prefix row had no metadata entries; the reader must not surface an (empty) map for it. + assertNull(prefix.getMetadata()); + assertFalse(items.isEmpty()); + } + + @Test + public void allColumnsFixtureDecodesWithoutRejection() throws IOException { + // Every column the SDK claims to handle is present in this full-schema fixture and must decode without the + // unknown-column guard firing (no false positives) and without a type-mismatch error. + ArrowBlobListDeserializer.ArrowListBlobsResult result + = ArrowBlobListDeserializer.deserialize(openFixture("allcolumns.arrow.base64")); + + List items = result.getBlobItems(); + assertEquals(1, items.size()); + BlobItemInternal blob = items.get(0); + + // Spot-check one field from each Arrow type category to prove full-schema decode. + assertEquals("blob1", blob.getName().getContent()); + assertEquals(Boolean.FALSE, blob.isDeleted()); + assertEquals(7L, (long) blob.getProperties().getContentLength()); + assertEquals(1000L, blob.getProperties().getCreationTime().toEpochSecond()); + assertEquals("mv", blob.getMetadata().get("mk")); + } + + @Test + public void allColumnsFixtureSchemaMatchesKnownColumnSet() throws IOException { + // Drift guard: the frozen full-schema fixture and the deserializer's known-column set must stay identical. If a + // column is added to KNOWN_COLUMNS without regenerating the fixture (or vice versa), this fails loudly. + Set fixtureColumns; + try (InputStream stream = openFixture("allcolumns.arrow.base64")) { + BlobListArrowStreamReader.DecodedArrowStream decodedArrowStream = BlobListArrowStreamReader.read(stream); + fixtureColumns = decodedArrowStream.getBatches().get(0).getColumnNames(); + } + assertEquals(ArrowBlobListDeserializer.knownColumns(), fixtureColumns); + } + + @Test + public void rejectsUnknownColumn() throws IOException { + // The service added a column a future SDK understands; this SDK must reject loudly and name the offender. + try (InputStream stream = openFixture("reject-unknown-column.arrow.base64")) { + BlobListArrowParseException ex + = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream)); + assertTrue(ex.getMessage().contains("FutureField"), "Unexpected message: " + ex.getMessage()); + } + } + + private static InputStream openFixture(String name) throws IOException { + byte[] base64; + try (InputStream resource = classpathResource("/arrow/" + name)) { + assertNotNull(resource, "missing test fixture: arrow/" + name); + base64 = readAll(resource); + } + return new ByteArrayInputStream(Base64.getDecoder().decode(base64)); + } + + /** + * Loads a resource from the test classpath. Calling {@code getResourceAsStream} on this test's class literal is + * equivalent to going through its class loader directly (like + * {@code Thread.currentThread().getContextClassLoader().getResourceAsStream(...)}); we use the class literal because + * this is a static context with no {@code this} on which to call {@code getClass()}. The leading {@code /} makes the + * path absolute from the classpath root. + * + * @param absolutePath the classpath-absolute resource path, beginning with {@code /} + * @return the resource stream, or null if the resource is not found + */ + private static InputStream classpathResource(String absolutePath) { + return BlobListArrowGoldenDecodeTests.class.getResourceAsStream(absolutePath); + } + + private static byte[] readAll(InputStream in) throws IOException { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java new file mode 100644 index 000000000000..349db1cefa48 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.storage.blob.implementation.models.BlobListArrowParseException; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Parity tests for Arrow IPC content the ListBlobs reader intentionally rejects or treats as an error. The payloads + * are committed golden fixtures (see {@code src/test/resources/arrow}) originally produced with the official + * {@code arrow-vector} writer, so these tests validate the vendored reader's rejection paths against genuine Arrow + * output with no Arrow dependency. The remaining cases cover malformed/edge inputs that the writer cannot produce + * (null and empty streams). + */ +public class BlobListArrowStreamReaderRejectionTests { + + @Test + public void rejectsNullStream() { + BlobListArrowParseException ex + = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(null)); + assertTrue(ex.getMessage().contains("input stream is null"), "Unexpected message: " + ex.getMessage()); + } + + @Test + public void rejectsStreamWithNoSchema() { + InputStream stream = new ByteArrayInputStream(new byte[0]); + BlobListArrowParseException ex + = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream)); + assertTrue(ex.getMessage().contains("stream contained no schema"), "Unexpected message: " + ex.getMessage()); + } + + @Test + public void rejectsDictionaryEncodedStreams() throws IOException { + assertRejected("reject-dictionary.arrow.base64", "dictionary-encoded streams are not supported"); + } + + @Test + public void rejectsUnsupportedColumnType() throws IOException { + assertRejected("reject-float.arrow.base64", "unsupported Arrow type 'FloatingPoint'"); + } + + @Test + public void rejectsUnsupportedTimestampUnit() throws IOException { + assertRejected("reject-timestamp-milli.arrow.base64", "unsupported timestamp unit 'MILLISECOND'"); + } + + @Test + public void rejectsMapWithNonStringValues() throws IOException { + assertRejected("reject-map-nonstring.arrow.base64", "map entries must be string keys and values"); + } + + // region helpers + + private static void assertRejected(String fixture, String expectedMessageFragment) throws IOException { + try (InputStream stream = openFixture(fixture)) { + BlobListArrowParseException ex + = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream)); + assertTrue(ex.getMessage().contains(expectedMessageFragment), "Unexpected message: " + ex.getMessage()); + } + } + + private static InputStream openFixture(String name) throws IOException { + byte[] base64; + try (InputStream resource + = BlobListArrowStreamReaderRejectionTests.class.getResourceAsStream("/arrow/" + name)) { + assertNotNull(resource, "missing test fixture: arrow/" + name); + base64 = readAll(resource); + } + return new ByteArrayInputStream(Base64.getDecoder().decode(base64)); + } + + private static byte[] readAll(InputStream in) throws IOException { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } + + //endregion +} diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md b/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md new file mode 100644 index 000000000000..747f82ae3a7f --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/README.md @@ -0,0 +1,13 @@ +# ListBlobs Arrow golden fixtures + +These `*.arrow.base64` files are committed golden fixtures for the vendored ListBlobs Arrow +deserializer tests (`BlobListArrowGoldenDecodeTests`, `BlobListArrowStreamReaderRejectionTests`). +Each one is an Apache Arrow IPC stream that has been **Base64-encoded** before being written to disk. + +The tests decode the Base64 back to the original Arrow bytes before parsing, so the on-disk +encoding is transparent to the deserializer under test. + +## Naming + +`.arrow.base64` — `.arrow` denotes the Apache Arrow IPC stream; `.base64` denotes the +Base64 text wrapper. diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64 new file mode 100644 index 000000000000..44eeb10f79cc --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/allcolumns.arrow.base64 @@ -0,0 +1 @@ +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AAAAAAJgBAAAAAAAAAQAAAAAAAACgAQAAAAAAAAgAAAAAAAAAqAEAAAAAAAABAAAAAAAAALABAAAAAAAACAAAAAAAAAC4AQAAAAAAAAEAAAAAAAAAwAEAAAAAAAAIAAAAAAAAAMgBAAAAAAAAEQAAAAAAAADgAQAAAAAAAAEAAAAAAAAA6AEAAAAAAAAIAAAAAAAAAPABAAAAAAAAAQAAAAAAAAD4AQAAAAAAAAgAAAAAAAAAAAIAAAAAAAAYAAAAAAAAABgCAAAAAAAAAQAAAAAAAAAgAgAAAAAAAAgAAAAAAAAAKAIAAAAAAAAEAAAAAAAAADACAAAAAAAAAQAAAAAAAAA4AgAAAAAAAAgAAAAAAAAAQAIAAAAAAAAFAAAAAAAAAEgCAAAAAAAAAQAAAAAAAABQAgAAAAAAAAgAAAAAAAAAWAIAAAAAAAAGAAAAAAAAAGACAAAAAAAAAQAAAAAAAABoAgAAAAAAAAgAAAAAAAAAcAIAAAAAAAAIAAAAAAAAAHgCAAAAAAAAAQAAAAAAAACAAgAAAAAAAAgAAAAAAAAAiAIAAAAAAAAIAAAAAAAAAJACAAAAAAAAAQAAAAAAAACYAgAAAAAAAAgAAAAAAAAAoAIAAAAAAAAJAAAAAAAAALACAAAAAAAAAQAAAAAAAAC4AgAAAAAAAAgAAAAAAAAAwAIAAAAAAAADAAAAAAAAAMgCAAAAAAAAAQAAAAAAAADQAgAAAAAAAAEAAAAAAAAA2AIAAAAAAAABAAAAAAAAAOACAAAAAAAACAAAAAAAAADoAgAAAAAAAAEAAAAAAAAA8AIAAAAAAAAIAAAAAAAAAPgCAAAAAAAABAAAAAAAAAAAAwAAAAAAAAEAAAAAAAAACAMAAAAAAAAIAAAAAAAAABADAAAAAAAACAAAAAAAAAAYAwAAAAAAAAEAAAAAAAAAIAMAAAAAAAAIAAAAAAAAACgDAAAAAAAACQAAAAAAAAA4AwAAAAAAAAEAAAAAAAAAQAMAAAAAAAAIAAAAAAAAAEgDAAAAAAAACAAAAAAAAABQAwAAAAAAAAEAAAAAAAAAWAMAAAAAAAABAAAAAAAAAGADAAAAAAAAAQAAAAAAAABoAwAAAAAAAAgAAAAAAAAAcAMAAAAAAAArAAAAAAAAAKADAAAAAAAAAQAAAAAAAACoAwAAAAAAAAgAAAAAAAAAsAMAAAAAAAAGAAAAAAAAALgDAAAAAAAAAQAAAAAAAADAAwAAAAAAAAEAAAAAAAAAyAMAAAAAAAABAAAAAAAAANADAAAAAAAACAAAAAAAAADYAwAAAAAAACQAAAAAAAAAAAQAAAAAAAABAAAAAAAAAAgEAAAAAAAACAAAAAAAAAAQBAAAAAAAAAcAAAAAAAAAGAQAAAAAAAABAAAAAAAAACAEAAAAAAAACAAAAAAAAAAoBAAAAAAAADYAAAAAAAAAYAQAAAAAAAABAAAAAAAAAGgEAAAAAAAACAAAAAAAAABwBAAAAAAAAAMAAAAAAAAAeAQAAAAAAAABAAAAAAAAAIAEAAAAAAAACAAAAAAAAACIBAAAAAAAAAEAAAAAAAAAkAQAAAAAAAAIAAAAAAAAAJgEAAAAAAAADQAAAAAAAACoBAAAAAAAAAEAAAAAAAAAsAQAAAAAAAAIAAAAAAAAALgEAAAAAAAAHAAAAAAAAADYBAAAAAAAAAEAAAAAAAAA4AQAAAAAAAAIAAAAAAAAAOgEAAAAAAAAAQAAAAAAAADwBAAAAAAAAAEAAAAAAAAA+AQAAAAAAAABAAAAAAAAAAAFAAAAAAAAAQAAAAAAAAAIBQAAAAAAAAEAAAAAAAAAEAUAAAAAAAAIAAAAAAAAABgFAAAAAAAAAQAAAAAAAAAgBQAAAAAAAAgAAAAAAAAAKAUAAAAAAAABAAAAAAAAADAFAAAAAAAACAAAAAAAAAA4BQAAAAAAAAEAAAAAAAAAQAUAAAAAAAAIAAAAAAAAAEgFAAAAAAAACAAAAAAAAABQBQAAAAAAAAEAAAAAAAAAWAUAAAAAAAAIAAAAAAAAAGAFAAAAAAAAGAAAAAAAAAB4BQAAAAAAAAEAAAAAAAAAgAUAAAAAAAAIAAAAAAAAAIgFAAAAAAAABAAAAAAAAACQBQAAAAAAAAEAAAAAAAAAmAUAAAAAAAAIAAAAAAAAAKAFAAAAAAAAAQAAAAAAAACoBQAAAAAAAAgAAAAAAAAAAAAAADsAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAUAAABibG9iMQAAAAEAAAAAAAAAAAAAAAQAAABibG9iAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMVQwMDowMDowMC4wMDAwMDAwWgAAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMVQwMDowMDowMC4wMDAwMDAxWgAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAABtawAAAAAAAAEAAAAAAAAAAAAAAAIAAABtdgAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAABvawAAAAAAAAEAAAAAAAAAAAAAAAIAAABvdgAAAAAAAAEAAAAAAAAAAAAAAAEAAAABAAAAAAAAAAEAAAAAAAAAAAAAAAIAAAB0awAAAAAAAAEAAAAAAAAAAAAAAAIAAAB0dgAAAAAAAAEAAAAAAAAA6AMAAAAAAAABAAAAAAAAANAHAAAAAAAAAQAAAAAAAAAAAAAAEQAAADB4OEQ4RDhEOEQ4RDhEOEQ4AAAAAAAAAAEAAAAAAAAABwAAAAAAAAABAAAAAAAAAAAAAAAYAAAAYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtAQAAAAAAAAAAAAAABAAAAGd6aXAAAAAAAQAAAAAAAAAAAAAABQAAAGVuLVVTAAAAAQAAAAAAAAAAAAAABgAAAGlubGluZQAAAQAAAAAAAAAAAAAACAAAAG5vLWNhY2hlAQAAAAAAAAAAAAAACAAAAEFRSURCQT09AQAAAAAAAAAAAAAACQAAAEJsb2NrQmxvYgAAAAAAAAABAAAAAAAAAAAAAAADAAAASG90AAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAC4CwAAAAAAAAEAAAAAAAAAAAAAAAQAAABDb29sAAAAAAEAAAAAAAAAAAAAAAgAAAB1bmxvY2tlZAEAAAAAAAAAAAAAAAkAAABhdmFpbGFibGUAAAAAAAAAAQAAAAAAAAAAAAAACAAAAGluZmluaXRlAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAAAAAAACsAAABQbXQwUjBrazJ2N3E1cUIxUW5ublE0eFEwUTBRMFEwUTBRMFEwUTBRMFE9AAAAAAABAAAAAAAAAAAAAAAGAAAAc2NvcGUxAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAJAAAADAwMDAwMDAwLTAwMDAtMDAwMC0wMDAwLTAwMDAwMDAwMDAwMQAAAAABAAAAAAAAAAAAAAAHAAAAc3VjY2VzcwABAAAAAAAAAAAAAAA2AAAAaHR0cHM6Ly9hY2NvdW50LmJsb2IuY29yZS53aW5kb3dzLm5ldC9jb250YWluZXIvc291cmNlAAABAAAAAAAAAAAAAAADAAAANy83AAAAAAABAAAAAAAAAKAPAAAAAAAAAQAAAAAAAAAAAAAADQAAAGNvcHkgY29tcGxldGUAAAABAAAAAAAAAAAAAAAcAAAAMjAyMC0wMS0wMlQwMDowMDowMC4wMDAwMDAwWgAAAAABAAAAAAAAACoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAIgTAAAAAAAAAQAAAAAAAABwFwAAAAAAAAEAAAAAAAAAWBsAAAAAAAABAAAAAAAAAAAAAAAIAAAAdW5sb2NrZWQBAAAAAAAAAAAAAAAYAAAAcmVoeWRyYXRlLXBlbmRpbmctdG8taG90AQAAAAAAAAAAAAAABAAAAEhpZ2gAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAAAAAABQAAAAAAAAD/////AAAAAA== \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64 new file mode 100644 index 000000000000..a877f2b8d963 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-dictionary.arrow.base64 @@ -0,0 +1 @@ +/////7gAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABwAGAAXABYAEAAEAAwACAASAAAAKAAAABQAAAAUAAAARAAAAAAABQFAAAAAAAAAAAAAAAAIABAACAAEAAgAAAAUAAAAAQAAAAAAAAAIAAwACAAHAAgAAAAAAAABIAAAAAQABAAEAAAABAAAAE5hbWUAAAAA/////7gAAAAUAAAAAAAAAAwAHAASABsAFAAEAAwAAAAoAAAAAAAAAAAAAAAAAAQAEAAAAAAAAAIIABIACAAEAAgAAAAYAAAAAQAAAAAAAAAAAAoAGAAMAAgABAAKAAAAFAAAAEgAAAACAAAAAAAAAAAAAAADAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAADAAAAAAAAAAYAAAAAAAAAAoAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAABQAAAAoAAAAAAAAAYmxvYjFibG9iMgAAAAAAAP////+IAAAAFAAAAAAAAAAMABYADgAVABAABAAMAAAAEAAAAAAAAAAAAAQAEAAAAAADCgAYAAwACAAEAAoAAAAUAAAAOAAAAAIAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAEAAAD/////AAAAAA== \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64 new file mode 100644 index 000000000000..8065f28604cf --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-float.arrow.base64 @@ -0,0 +1 @@ +/////5AAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAABQAAAAcAAAAAAADARwAAAAAAAAAAAAAAAAABgAIAAYABgAAAAAAAgAFAAAAU2NvcmUAAAD/////iAAAABQAAAAAAAAADAAWAA4AFQAQAAQADAAAABAAAAAAAAAAAAAEABAAAAAAAwoAGAAMAAgABAAKAAAAFAAAADgAAAABAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAPg//////wAAAAA= \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64 new file mode 100644 index 000000000000..9a26eb1d998a --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-map-nonstring.arrow.base64 @@ -0,0 +1 @@ +/////0gBAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAEAAAAsv///xQAAAAUAAAA5AAAAAAAEQHgAAAAAAAAAAEAAAAEAAAAhv///xQAAAAUAAAArAAAAAAAAA2oAAAAAAAAAAIAAABsAAAAGAAAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAAAUAAAAHAAAAAAAAgEgAAAAAAAAAAAAAAAIAAwACAAHAAgAAAAAAAABQAAAAAUAAAB2YWx1ZQASABgAFAAAABMADAAAAAgABAASAAAAFAAAABQAAAAUAAAAAAAABRAAAAAAAAAAAAAAAOT///8DAAAAa2V5APD///8HAAAAZW50cmllcwAEAAQABAAAAAgAAABNZXRhZGF0YQAAAAAAAAAA/////xgBAAAUAAAAAAAAAAwAFgAOABUAEAAEAAwAAABAAAAAAAAAAAAABAAQAAAAAAMKABgADAAIAAQACgAAABQAAACYAAAAAQAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAQAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAEAAAAAAAAAABAAAAAAAAABgAAAAAAAAAAQAAAAAAAAAgAAAAAAAAAAgAAAAAAAAAKAAAAAAAAAACAAAAAAAAADAAAAAAAAAAAQAAAAAAAAA4AAAAAAAAAAgAAAAAAAAAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAgAAAGsxAAAAAAAAAQAAAAAAAAAqAAAAAAAAAP////8AAAAA \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64 new file mode 100644 index 000000000000..58c6263aa995 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-timestamp-milli.arrow.base64 @@ -0,0 +1 @@ +/////5gAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAEAAAAYAAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAABQAAAAcAAAAAAAKARwAAAAAAAAAAAAAAAAABgAIAAYABgAAAAAAAQANAAAAQ3JlYXRpb24tVGltZQAAAP////+IAAAAFAAAAAAAAAAMABYADgAVABAABAAMAAAAEAAAAAAAAAAAAAQAEAAAAAADCgAYAAwACAAEAAoAAAAUAAAAOAAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAEAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAA6AMAAAAAAAD/////AAAAAA== \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64 new file mode 100644 index 000000000000..281e0d3be3da --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/reject-unknown-column.arrow.base64 @@ -0,0 +1 @@ +/////8AAAAAQAAAAAAAKAA4ABgANAAgACgAAAAAABAAQAAAAAAEKAAwAAAAIAAQACgAAAAgAAAAIAAAAAAAAAAIAAABQAAAABAAAAMr///8UAAAAFAAAABQAAAAAAAUBEAAAAAAAAAAAAAAAuP///wsAAABGdXR1cmVGaWVsZAAAABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAAFAAAABgAAAAAAAUBFAAAAAAAAAAAAAAABAAEAAQAAAAEAAAATmFtZQAAAAD/////2AAAABQAAAAAAAAADAAWAA4AFQAQAAQADAAAADAAAAAAAAAAAAAEABAAAAAAAwoAGAAMAAgABAAKAAAAFAAAAHgAAAABAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAABAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAQAAAAAAAAAAUAAAAAAAAAGAAAAAAAAAABAAAAAAAAACAAAAAAAAAACAAAAAAAAAAoAAAAAAAAAAgAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAUAAABibG9iMQAAAAEAAAAAAAAAAAAAAAgAAABzdXJwcmlzZf////8AAAAA \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64 b/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64 new file mode 100644 index 000000000000..7e43432b368e --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/resources/arrow/representative.arrow.base64 @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/sdk/storage/azure-storage-blob/swagger/README.md b/sdk/storage/azure-storage-blob/swagger/README.md index 50accb697f5a..0019d026a0d0 100644 --- a/sdk/storage/azure-storage-blob/swagger/README.md +++ b/sdk/storage/azure-storage-blob/swagger/README.md @@ -16,7 +16,7 @@ autorest ### Code generation settings ``` yaml use: '@autorest/java@4.1.63' -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/52424fa89006699e353d864e8b04600ed84aeb6d/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-12-06/blob.json +input-file: https://raw.githubusercontent.com/nickliu-msft/azure-rest-api-specs/f85584d452061985a5fc21a67b8fc0b46b75188a/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json java: true output-folder: ../ namespace: com.azure.storage.blob diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java index 687e80c71961..fd72f67021d5 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/Constants.java @@ -291,6 +291,14 @@ private HeaderConstants() { } } + public static final class ContentTypeConstants { + public static final String APPLICATION_VND_APACHE_ARROW_STREAM = "application/vnd.apache.arrow.stream"; + + private ContentTypeConstants() { + // Private to prevent construction. + } + } + /** * Defines constants for use with URLs. * diff --git a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java index 90aad19fffbb..081ea1f2df72 100644 --- a/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java +++ b/sdk/storage/azure-storage-common/src/main/java/com/azure/storage/common/implementation/StorageImplUtils.java @@ -397,6 +397,19 @@ public static String convertStorageExceptionMessage(String message, HttpResponse return message; } + /** + * Checks whether the provided header value is non-null, non-empty, and starts with the expected value. + * + * @param headerValue The header value to inspect. + * @param expectedHeaderValue The expected header value prefix. + * @return {@code true} if the header value is present and matches the expected value; otherwise {@code false}. + */ + public static boolean hasMatchingHeaderValue(String headerValue, String expectedHeaderValue) { + return !CoreUtils.isNullOrEmpty(headerValue) + && !CoreUtils.isNullOrEmpty(expectedHeaderValue) + && headerValue.startsWith(expectedHeaderValue); + } + /** * Given a String representing a date in a form of the ISO8601 pattern, generates a Date representing it with up to * millisecond precision.