From 78c44ea0b60a3c62fb7cf8817ee1d17ca3e268c4 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Fri, 17 Apr 2026 12:08:09 +0200 Subject: [PATCH] CAMEL-23328: Add support for blob index tags (set and get) to camel-azure-storage-blob Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Andrea Cosentino --- .../components/azure-storage-blob.json | 9 +- .../storage/blob/azure-storage-blob.json | 9 +- .../docs/azure-storage-blob-component.adoc | 24 +++++ .../azure/storage/blob/BlobConfiguration.java | 2 +- .../blob/BlobConfigurationOptionsProxy.java | 4 + .../azure/storage/blob/BlobConstants.java | 4 + .../storage/blob/BlobExchangeHeaders.java | 10 ++ .../blob/BlobOperationsDefinition.java | 11 ++- .../azure/storage/blob/BlobProducer.java | 6 ++ .../blob/client/BlobClientWrapper.java | 23 +++++ .../blob/operations/BlobOperations.java | 50 ++++++++++ .../blob/integration/BlobOperationsIT.java | 99 +++++++++++++++++++ .../blob/operations/BlobOperationsTest.java | 80 +++++++++++++++ .../dsl/BlobEndpointBuilderFactory.java | 13 +++ 14 files changed, 334 insertions(+), 10 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json index cb990329ee740..34329a989aa42 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/azure-storage-blob.json @@ -64,7 +64,7 @@ "lazyStartProducer": { "index": 37, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, "maxConcurrency": { "index": 38, "kind": "property", "displayName": "Max Concurrency", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum number of parallel requests to use during upload with uploadBlockBlobChunked operation. Default is determined by the Azure SDK based on available processors." }, "maxSingleUploadSize": { "index": 39, "kind": "property", "displayName": "Max Single Upload Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum size in bytes for a single upload request with uploadBlockBlobChunked operation. Files smaller than this will be uploaded in a single request. Files larger will use chunked upload with blocks of size blockSize. Default is 256MB." }, - "operation": { "index": 40, "kind": "property", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, + "operation": { "index": 40, "kind": "property", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, "pageBlobSize": { "index": 41, "kind": "property", "displayName": "Page Blob Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 512, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Specifies the maximum size for the page blob, up to 8 TB. The page blob size must be aligned to a 512-byte boundary." }, "autowiredEnabled": { "index": 42, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, "healthCheckConsumerEnabled": { "index": 43, "kind": "property", "displayName": "Health Check Consumer Enabled", "group": "health", "label": "health", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Used for enabling or disabling all consumer based health checks from this component" }, @@ -76,7 +76,7 @@ "sourceBlobAccessKey": { "index": 49, "kind": "property", "displayName": "Source Blob Access Key", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Source Blob Access Key: for copyblob operation, sadly, we need to have an accessKey for the source blob we want to copy Passing an accessKey as header, it's unsafe so we could set as key." } }, "headers": { - "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(All) Specify the producer operation to execute, please see the doc on this page related to producer operation.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_OPERATION" }, + "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(All) Specify the producer operation to execute, please see the doc on this page related to producer operation.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_OPERATION" }, "CamelAzureStorageBlobHttpHeaders": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "BlobHttpHeaders", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(uploadBlockBlob, commitBlobBlockList, createAppendBlob, createPageBlob) Additional parameters for a set of operations.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_HTTP_HEADERS" }, "CamelAzureStorageBlobETag": { "index": 2, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The E Tag of the blob", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#E_TAG" }, "CamelAzureStorageBlobCreationTime": { "index": 3, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creation time of the blob.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CREATION_TIME" }, @@ -145,7 +145,8 @@ "CamelAzureStorageBlobChangeFeedStartTime": { "index": 66, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) It filters the results to return events approximately after the start time. Note: A few events belonging to the previous hour can also be returned. A few events belonging to this hour can be missing; to ensure all events from the hour are returned, round the start time down by an hour.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_START_TIME" }, "CamelAzureStorageBlobChangeFeedEndTime": { "index": 67, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) It filters the results to return events approximately before the end time. Note: A few events belonging to the next hour can also be returned. A few events belonging to this hour can be missing; to ensure all events from the hour are returned, round the end time up by an hour.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_END_TIME" }, "CamelAzureStorageBlobContext": { "index": 68, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Context", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) This gives additional context that is passed through the Http pipeline during the service call.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_CONTEXT" }, - "CamelAzureStorageBlobSnapshotId": { "index": 69, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The snapshot identifier. On createBlobSnapshot it is set on the exchange as the id of the newly created snapshot. On read operations (getBlob, downloadBlobToFile, downloadLink) it can be provided as input to target a specific blob snapshot.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_SNAPSHOT_ID" } + "CamelAzureStorageBlobSnapshotId": { "index": 69, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The snapshot identifier. On createBlobSnapshot it is set on the exchange as the id of the newly created snapshot. On read operations (getBlob, downloadBlobToFile, downloadLink) it can be provided as input to target a specific blob snapshot.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_SNAPSHOT_ID" }, + "CamelAzureStorageBlobTags": { "index": 70, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(producer) (setBlobTags) The tags to set on the blob as key-value pairs. (consumer) The tags retrieved from the blob.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_TAGS" } }, "properties": { "accountName": { "index": 0, "kind": "path", "displayName": "Account Name", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Azure account name to be used for authentication with azure blob services" }, @@ -193,7 +194,7 @@ "downloadLinkExpiration": { "index": 42, "kind": "parameter", "displayName": "Download Link Expiration", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Override the default expiration (millis) of URL download link." }, "maxConcurrency": { "index": 43, "kind": "parameter", "displayName": "Max Concurrency", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum number of parallel requests to use during upload with uploadBlockBlobChunked operation. Default is determined by the Azure SDK based on available processors." }, "maxSingleUploadSize": { "index": 44, "kind": "parameter", "displayName": "Max Single Upload Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum size in bytes for a single upload request with uploadBlockBlobChunked operation. Files smaller than this will be uploaded in a single request. Files larger will use chunked upload with blocks of size blockSize. Default is 256MB." }, - "operation": { "index": 45, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, + "operation": { "index": 45, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, "pageBlobSize": { "index": 46, "kind": "parameter", "displayName": "Page Blob Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 512, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Specifies the maximum size for the page blob, up to 8 TB. The page blob size must be aligned to a 512-byte boundary." }, "lazyStartProducer": { "index": 47, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, "backoffErrorThreshold": { "index": 48, "kind": "parameter", "displayName": "Backoff Error Threshold", "group": "scheduler", "label": "consumer,scheduler", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "description": "The number of subsequent error polls (failed due some error) that should happen before the backoffMultipler should kick-in." }, diff --git a/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json b/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json index cb990329ee740..34329a989aa42 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json +++ b/components/camel-azure/camel-azure-storage-blob/src/generated/resources/META-INF/org/apache/camel/component/azure/storage/blob/azure-storage-blob.json @@ -64,7 +64,7 @@ "lazyStartProducer": { "index": 37, "kind": "property", "displayName": "Lazy Start Producer", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, "maxConcurrency": { "index": 38, "kind": "property", "displayName": "Max Concurrency", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum number of parallel requests to use during upload with uploadBlockBlobChunked operation. Default is determined by the Azure SDK based on available processors." }, "maxSingleUploadSize": { "index": 39, "kind": "property", "displayName": "Max Single Upload Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum size in bytes for a single upload request with uploadBlockBlobChunked operation. Files smaller than this will be uploaded in a single request. Files larger will use chunked upload with blocks of size blockSize. Default is 256MB." }, - "operation": { "index": 40, "kind": "property", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, + "operation": { "index": 40, "kind": "property", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, "pageBlobSize": { "index": 41, "kind": "property", "displayName": "Page Blob Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 512, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Specifies the maximum size for the page blob, up to 8 TB. The page blob size must be aligned to a 512-byte boundary." }, "autowiredEnabled": { "index": 42, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." }, "healthCheckConsumerEnabled": { "index": 43, "kind": "property", "displayName": "Health Check Consumer Enabled", "group": "health", "label": "health", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Used for enabling or disabling all consumer based health checks from this component" }, @@ -76,7 +76,7 @@ "sourceBlobAccessKey": { "index": 49, "kind": "property", "displayName": "Source Blob Access Key", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Source Blob Access Key: for copyblob operation, sadly, we need to have an accessKey for the source blob we want to copy Passing an accessKey as header, it's unsafe so we could set as key." } }, "headers": { - "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(All) Specify the producer operation to execute, please see the doc on this page related to producer operation.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_OPERATION" }, + "CamelAzureStorageBlobOperation": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(All) Specify the producer operation to execute, please see the doc on this page related to producer operation.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_OPERATION" }, "CamelAzureStorageBlobHttpHeaders": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "BlobHttpHeaders", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(uploadBlockBlob, commitBlobBlockList, createAppendBlob, createPageBlob) Additional parameters for a set of operations.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_HTTP_HEADERS" }, "CamelAzureStorageBlobETag": { "index": 2, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The E Tag of the blob", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#E_TAG" }, "CamelAzureStorageBlobCreationTime": { "index": 3, "kind": "header", "displayName": "", "group": "consumer", "label": "consumer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Creation time of the blob.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CREATION_TIME" }, @@ -145,7 +145,8 @@ "CamelAzureStorageBlobChangeFeedStartTime": { "index": 66, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) It filters the results to return events approximately after the start time. Note: A few events belonging to the previous hour can also be returned. A few events belonging to this hour can be missing; to ensure all events from the hour are returned, round the start time down by an hour.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_START_TIME" }, "CamelAzureStorageBlobChangeFeedEndTime": { "index": 67, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "OffsetDateTime", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) It filters the results to return events approximately before the end time. Note: A few events belonging to the next hour can also be returned. A few events belonging to this hour can be missing; to ensure all events from the hour are returned, round the end time up by an hour.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_END_TIME" }, "CamelAzureStorageBlobContext": { "index": 68, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Context", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(getChangeFeed) This gives additional context that is passed through the Http pipeline during the service call.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#CHANGE_FEED_CONTEXT" }, - "CamelAzureStorageBlobSnapshotId": { "index": 69, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The snapshot identifier. On createBlobSnapshot it is set on the exchange as the id of the newly created snapshot. On read operations (getBlob, downloadBlobToFile, downloadLink) it can be provided as input to target a specific blob snapshot.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_SNAPSHOT_ID" } + "CamelAzureStorageBlobSnapshotId": { "index": 69, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The snapshot identifier. On createBlobSnapshot it is set on the exchange as the id of the newly created snapshot. On read operations (getBlob, downloadBlobToFile, downloadLink) it can be provided as input to target a specific blob snapshot.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_SNAPSHOT_ID" }, + "CamelAzureStorageBlobTags": { "index": 70, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "(producer) (setBlobTags) The tags to set on the blob as key-value pairs. (consumer) The tags retrieved from the blob.", "constantName": "org.apache.camel.component.azure.storage.blob.BlobConstants#BLOB_TAGS" } }, "properties": { "accountName": { "index": 0, "kind": "path", "displayName": "Account Name", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Azure account name to be used for authentication with azure blob services" }, @@ -193,7 +194,7 @@ "downloadLinkExpiration": { "index": 42, "kind": "parameter", "displayName": "Download Link Expiration", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Override the default expiration (millis) of URL download link." }, "maxConcurrency": { "index": 43, "kind": "parameter", "displayName": "Max Concurrency", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum number of parallel requests to use during upload with uploadBlockBlobChunked operation. Default is determined by the Azure SDK based on available processors." }, "maxSingleUploadSize": { "index": 44, "kind": "parameter", "displayName": "Max Single Upload Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The maximum size in bytes for a single upload request with uploadBlockBlobChunked operation. Files smaller than this will be uploaded in a single request. Files larger will use chunked upload with blocks of size blockSize. Default is 256MB." }, - "operation": { "index": 45, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, + "operation": { "index": 45, "kind": "parameter", "displayName": "Operation", "group": "producer", "label": "producer", "required": false, "type": "enum", "javaType": "org.apache.camel.component.azure.storage.blob.BlobOperationsDefinition", "enum": [ "listBlobContainers", "createBlobContainer", "deleteBlobContainer", "listBlobs", "getBlob", "deleteBlob", "downloadBlobToFile", "downloadLink", "uploadBlockBlob", "uploadBlockBlobChunked", "stageBlockBlobList", "commitBlobBlockList", "getBlobBlockList", "createAppendBlob", "commitAppendBlob", "createPageBlob", "uploadPageBlob", "resizePageBlob", "clearPageBlob", "getPageBlobRanges", "getChangeFeed", "copyBlob", "createBlobSnapshot", "setBlobTags", "getBlobTags" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "listBlobContainers", "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "The blob operation that can be used with this component on the producer" }, "pageBlobSize": { "index": 46, "kind": "parameter", "displayName": "Page Blob Size", "group": "producer", "label": "producer", "required": false, "type": "integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": 512, "configurationClass": "org.apache.camel.component.azure.storage.blob.BlobConfiguration", "configurationField": "configuration", "description": "Specifies the maximum size for the page blob, up to 8 TB. The page blob size must be aligned to a 512-byte boundary." }, "lazyStartProducer": { "index": 47, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, "backoffErrorThreshold": { "index": 48, "kind": "parameter", "displayName": "Backoff Error Threshold", "group": "scheduler", "label": "consumer,scheduler", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "autowired": false, "secret": false, "description": "The number of subsequent error polls (failed due some error) that should happen before the backoffMultipler should kick-in." }, diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc b/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc index fd2a86c064897..5907a188d92ae 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc +++ b/components/camel-azure/camel-azure-storage-blob/src/main/docs/azure-storage-blob-component.adoc @@ -165,6 +165,8 @@ and existing blocks together. Any blocks not specified in the block list and per |`getPageBlobRanges`|`PageBlob`|Returns the list of valid page ranges for a page blob or snapshot of a page blob. |`copyBlob`|`Common`|Copy a blob from one container to another one, even from different accounts. |`createBlobSnapshot`|`Common`|Creates a read-only snapshot of a blob. The snapshot ID is returned in the `CamelAzureStorageBlobSnapshotId` header. +|`setBlobTags`|`Common`|Sets user-defined index tags on a blob. Tags are key-value pairs that can be used to filter and query blobs across containers. Tags can be provided via the `CamelAzureStorageBlobTags` header or as the message body (`Map`). +|`getBlobTags`|`Common`|Retrieves user-defined index tags from a blob. The tags are returned as the message body (`Map`) and also set in the `CamelAzureStorageBlobTags` header. |=== Refer to the example section in this page to learn how to use these operations into your camel application. @@ -740,6 +742,28 @@ from("direct:readSnapshot") .to("mock:result"); -------------------------------------------------------------------------------- +- `setBlobTags` + +[source,java] +-------------------------------------------------------------------------------- + +from("direct:setBlobTags") + .setHeader(BlobConstants.BLOB_TAGS, constant(Map.of("status", "quarantine", "category", "document"))) + .to("azure-storage-blob://camelazure/container1?blobName=hello.txt&operation=setBlobTags&serviceClient=#client") + .to("mock:result"); +-------------------------------------------------------------------------------- + +- `getBlobTags` + +[source,java] +-------------------------------------------------------------------------------- + +from("direct:getBlobTags") + .to("azure-storage-blob://camelazure/container1?blobName=hello.txt&operation=getBlobTags&serviceClient=#client") + .log("Tags: ${body}") + .to("mock:result"); +-------------------------------------------------------------------------------- + === SAS Token generation example SAS Blob Container tokens can be generated programmatically or via Azure UI. To generate the token with java code, the following can be done: diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfiguration.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfiguration.java index 9b9534dd6881c..816b0581c3af0 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfiguration.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfiguration.java @@ -51,7 +51,7 @@ public class BlobConfiguration implements Cloneable { @UriParam(label = "producer", enums = "listBlobContainers,createBlobContainer,deleteBlobContainer,listBlobs,getBlob,deleteBlob,downloadBlobToFile,downloadLink," + "uploadBlockBlob,uploadBlockBlobChunked,stageBlockBlobList,commitBlobBlockList,getBlobBlockList,createAppendBlob,commitAppendBlob,createPageBlob,uploadPageBlob,resizePageBlob," - + "clearPageBlob,getPageBlobRanges,getChangeFeed,copyBlob,createBlobSnapshot", + + "clearPageBlob,getPageBlobRanges,getChangeFeed,copyBlob,createBlobSnapshot,setBlobTags,getBlobTags", defaultValue = "listBlobContainers") private BlobOperationsDefinition operation = BlobOperationsDefinition.listBlobContainers; @UriParam(label = "common") diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfigurationOptionsProxy.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfigurationOptionsProxy.java index de7e68634af32..70f38b032a20b 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfigurationOptionsProxy.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConfigurationOptionsProxy.java @@ -275,6 +275,10 @@ public ParallelTransferOptions getUploadParallelTransferOptions(final Exchange e return null; } + public Map getBlobTags(final Exchange exchange) { + return BlobExchangeHeaders.getBlobTagsFromHeaders(exchange); + } + public BlobConfiguration getConfiguration() { return configuration; } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConstants.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConstants.java index d936c26a7ca24..17b423a0c66ad 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConstants.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobConstants.java @@ -288,6 +288,10 @@ public final class BlobConstants { + " downloadLink) it can be provided as input to target a specific blob snapshot.", javaType = "String") public static final String BLOB_SNAPSHOT_ID = HEADER_PREFIX + "SnapshotId"; + @Metadata(description = "(producer) (setBlobTags) The tags to set on the blob as key-value pairs.\n" + + "(consumer) The tags retrieved from the blob.", + javaType = "Map") + public static final String BLOB_TAGS = HEADER_PREFIX + "Tags"; private BlobConstants() { } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobExchangeHeaders.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobExchangeHeaders.java index 75aa59eb50ad3..e4be71c4eab79 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobExchangeHeaders.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobExchangeHeaders.java @@ -485,4 +485,14 @@ public BlobExchangeHeaders snapshotId(final String snapshotId) { headers.put(BlobConstants.BLOB_SNAPSHOT_ID, snapshotId); return this; } + + @SuppressWarnings("unchecked") + public static Map getBlobTagsFromHeaders(final Exchange exchange) { + return getObjectFromHeaders(exchange, BlobConstants.BLOB_TAGS, Map.class); + } + + public BlobExchangeHeaders blobTags(final Map tags) { + headers.put(BlobConstants.BLOB_TAGS, tags); + return this; + } } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java index ecdac66f6c0a6..927fcd995937a 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobOperationsDefinition.java @@ -140,5 +140,14 @@ public enum BlobOperationsDefinition { /** * Creates a read-only snapshot of a blob. The snapshot ID is returned in the exchange headers. */ - createBlobSnapshot + createBlobSnapshot, + /** + * Sets user-defined index tags on a blob. Tags are key-value pairs that can be used to filter and query blobs + * across containers. + */ + setBlobTags, + /** + * Retrieves user-defined index tags from a blob. + */ + getBlobTags } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java index 601b8fec3c1c8..e7e38a0e54f68 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/BlobProducer.java @@ -132,6 +132,12 @@ public void process(final Exchange exchange) throws Exception { case createBlobSnapshot: setResponse(exchange, getBlobOperations(exchange).createBlobSnapshot(exchange)); break; + case setBlobTags: + setResponse(exchange, getBlobOperations(exchange).setBlobTags(exchange)); + break; + case getBlobTags: + setResponse(exchange, getBlobOperations(exchange).getBlobTags(exchange)); + break; default: throw new IllegalArgumentException("Unsupported operation"); } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/client/BlobClientWrapper.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/client/BlobClientWrapper.java index dca5c6254c4c6..8d777e09a5739 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/client/BlobClientWrapper.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/client/BlobClientWrapper.java @@ -53,7 +53,9 @@ import com.azure.storage.blob.models.PageRange; import com.azure.storage.blob.models.PageRangeItem; import com.azure.storage.blob.models.ParallelTransferOptions; +import com.azure.storage.blob.options.BlobGetTagsOptions; import com.azure.storage.blob.options.BlobParallelUploadOptions; +import com.azure.storage.blob.options.BlobSetTagsOptions; import com.azure.storage.blob.options.BlobUploadFromFileOptions; import com.azure.storage.blob.options.BlockBlobSimpleUploadOptions; import com.azure.storage.blob.options.ListPageRangesOptions; @@ -347,6 +349,27 @@ public BlobClientWrapper withSnapshot(final String snapshotId) { return new BlobClientWrapper(client.getSnapshotClient(snapshotId)); } + public Response setTags( + final Map tags, + final BlobRequestConditions requestConditions, + final Duration timeout) { + BlobSetTagsOptions options = new BlobSetTagsOptions(tags); + if (requestConditions != null) { + options.setRequestConditions(requestConditions); + } + return client.setTagsWithResponse(options, timeout, Context.NONE); + } + + public Response> getTags( + final BlobRequestConditions requestConditions, + final Duration timeout) { + BlobGetTagsOptions options = new BlobGetTagsOptions(); + if (requestConditions != null) { + options.setRequestConditions(requestConditions); + } + return client.getTagsWithResponse(options, timeout, Context.NONE); + } + public BlobLeaseClient getLeaseClient() { return new BlobLeaseClientBuilder().blobClient(client).buildClient(); } diff --git a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java index 915f3541726e7..44bca0b38a1ac 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java +++ b/components/camel-azure/camel-azure-storage-blob/src/main/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperations.java @@ -608,6 +608,56 @@ public BlobOperationResponse createBlobSnapshot(final Exchange exchange) { return BlobOperationResponse.create(snapshotClient.getSnapshotId(), exchangeHeaders.toMap()); } + @SuppressWarnings("unchecked") + public BlobOperationResponse setBlobTags(final Exchange exchange) { + ObjectHelper.notNull(exchange, MISSING_EXCHANGE); + + if (LOG.isTraceEnabled()) { + LOG.trace("Setting tags on blob [{}] from exchange [{}]...", configurationProxy.getBlobName(exchange), exchange); + } + + Map tags = configurationProxy.getBlobTags(exchange); + if (tags == null) { + tags = exchange.getIn().getBody(Map.class); + } + if (tags == null || tags.isEmpty()) { + throw new IllegalArgumentException( + "Tags must be specified either as the message body (Map) or via the " + + BlobConstants.BLOB_TAGS + " header."); + } + + final BlobCommonRequestOptions commonRequestOptions = getCommonRequestOptions(exchange); + + final Response response = client.setTags( + tags, + commonRequestOptions.getBlobRequestConditions(), + commonRequestOptions.getTimeout()); + + final BlobExchangeHeaders exchangeHeaders = BlobExchangeHeaders.create() + .httpHeaders(response.getHeaders()); + + return BlobOperationResponse.createWithEmptyBody(exchangeHeaders.toMap()); + } + + public BlobOperationResponse getBlobTags(final Exchange exchange) { + if (LOG.isTraceEnabled()) { + LOG.trace("Getting tags from blob [{}] from exchange [{}]...", configurationProxy.getBlobName(exchange), exchange); + } + + final BlobCommonRequestOptions commonRequestOptions = getCommonRequestOptions(exchange); + + final Response> response = client.getTags( + commonRequestOptions.getBlobRequestConditions(), + commonRequestOptions.getTimeout()); + + final Map tags = response.getValue(); + final BlobExchangeHeaders exchangeHeaders = BlobExchangeHeaders.create() + .blobTags(tags) + .httpHeaders(response.getHeaders()); + + return BlobOperationResponse.create(tags, exchangeHeaders.toMap()); + } + private DownloadRetryOptions getDownloadRetryOptions(final BlobConfigurationOptionsProxy configurationProxy) { return new DownloadRetryOptions().setMaxRetryRequests(configurationProxy.getMaxRetryRequests()); } diff --git a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/integration/BlobOperationsIT.java b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/integration/BlobOperationsIT.java index d0d02b8abceaa..1812e766075ef 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/integration/BlobOperationsIT.java +++ b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/integration/BlobOperationsIT.java @@ -28,8 +28,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.SecureRandom; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import com.azure.core.http.rest.PagedIterable; @@ -483,6 +485,103 @@ void testClearPages() throws Exception { blobClientWrapper.delete(null, null, null); } + @SuppressWarnings("unchecked") + @Test + void testSetAndGetBlobTags() { + final BlobClientWrapper blobClientWrapper = blobContainerClientWrapper.getBlobClientWrapper(randomBlobName); + final BlobOperations operations = new BlobOperations(configuration, blobClientWrapper); + + final Map tags = new HashMap<>(); + tags.put("status", "quarantine"); + tags.put("category", "document"); + tags.put("priority", "high"); + + // set tags via header + final Exchange setExchange = new DefaultExchange(context); + setExchange.getIn().setHeader(BlobConstants.BLOB_TAGS, tags); + + final BlobOperationResponse setResponse = operations.setBlobTags(setExchange); + assertNotNull(setResponse); + assertTrue((boolean) setResponse.getBody()); + + // get tags + final Exchange getExchange = new DefaultExchange(context); + final BlobOperationResponse getResponse = operations.getBlobTags(getExchange); + + assertNotNull(getResponse); + assertNotNull(getResponse.getBody()); + + final Map retrievedTags = (Map) getResponse.getBody(); + assertEquals(3, retrievedTags.size()); + assertEquals("quarantine", retrievedTags.get("status")); + assertEquals("document", retrievedTags.get("category")); + assertEquals("high", retrievedTags.get("priority")); + + // also verify tags are in exchange headers + assertEquals(retrievedTags, getResponse.getHeaders().get(BlobConstants.BLOB_TAGS)); + } + + @SuppressWarnings("unchecked") + @Test + void testSetBlobTagsFromBody() { + final BlobClientWrapper blobClientWrapper = blobContainerClientWrapper.getBlobClientWrapper(randomBlobName); + final BlobOperations operations = new BlobOperations(configuration, blobClientWrapper); + + final Map tags = new HashMap<>(); + tags.put("owner", "test-user"); + tags.put("scanned", "true"); + + // set tags via body + final Exchange setExchange = new DefaultExchange(context); + setExchange.getIn().setBody(tags); + + final BlobOperationResponse setResponse = operations.setBlobTags(setExchange); + assertNotNull(setResponse); + assertTrue((boolean) setResponse.getBody()); + + // verify by getting tags back + final Exchange getExchange = new DefaultExchange(context); + final BlobOperationResponse getResponse = operations.getBlobTags(getExchange); + + final Map retrievedTags = (Map) getResponse.getBody(); + assertEquals(2, retrievedTags.size()); + assertEquals("test-user", retrievedTags.get("owner")); + assertEquals("true", retrievedTags.get("scanned")); + } + + @SuppressWarnings("unchecked") + @Test + void testOverwriteBlobTags() { + final BlobClientWrapper blobClientWrapper = blobContainerClientWrapper.getBlobClientWrapper(randomBlobName); + final BlobOperations operations = new BlobOperations(configuration, blobClientWrapper); + + // set initial tags + final Map initialTags = new HashMap<>(); + initialTags.put("status", "pending"); + + final Exchange setExchange1 = new DefaultExchange(context); + setExchange1.getIn().setHeader(BlobConstants.BLOB_TAGS, initialTags); + operations.setBlobTags(setExchange1); + + // overwrite with new tags + final Map newTags = new HashMap<>(); + newTags.put("status", "processed"); + newTags.put("result", "clean"); + + final Exchange setExchange2 = new DefaultExchange(context); + setExchange2.getIn().setHeader(BlobConstants.BLOB_TAGS, newTags); + operations.setBlobTags(setExchange2); + + // verify only new tags are present (set replaces all tags) + final Exchange getExchange = new DefaultExchange(context); + final BlobOperationResponse getResponse = operations.getBlobTags(getExchange); + + final Map retrievedTags = (Map) getResponse.getBody(); + assertEquals(2, retrievedTags.size()); + assertEquals("processed", retrievedTags.get("status")); + assertEquals("clean", retrievedTags.get("result")); + } + @Test void testGetPageBlobRanges() throws Exception { final BlobClientWrapper blobClientWrapper = blobContainerClientWrapper.getBlobClientWrapper("upload_test_file.txt"); diff --git a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java index 7a81ef752f4d6..d1ff618813e4c 100644 --- a/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java +++ b/components/camel-azure/camel-azure-storage-blob/src/test/java/org/apache/camel/component/azure/storage/blob/operations/BlobOperationsTest.java @@ -255,6 +255,86 @@ void testGetBlobWithSnapshotIdHeaderOverridesConfig() throws IOException { verify(client, times(1)).withSnapshot(headerSnapshotId); } + @Test + void testSetBlobTags() { + final HttpHeaders httpHeaders = new HttpHeaders().set("x-test-header", "123"); + + when(client.setTags(any(), any(), any())) + .thenReturn(new ResponseBase<>(null, 204, httpHeaders, null, null)); + + final Map tags = new HashMap<>(); + tags.put("status", "quarantine"); + tags.put("category", "document"); + + final Exchange exchange = new DefaultExchange(context); + exchange.getIn().setHeader(BlobConstants.BLOB_TAGS, tags); + + final BlobOperations operations = new BlobOperations(configuration, client); + final BlobOperationResponse response = operations.setBlobTags(exchange); + + assertNotNull(response); + assertTrue((boolean) response.getBody()); + assertNotNull(response.getHeaders()); + assertEquals("123", ((HttpHeaders) response.getHeaders().get(BlobConstants.RAW_HTTP_HEADERS)) + .get("x-test-header").getValue()); + } + + @Test + void testSetBlobTagsFromBody() { + final HttpHeaders httpHeaders = new HttpHeaders().set("x-test-header", "456"); + + when(client.setTags(any(), any(), any())) + .thenReturn(new ResponseBase<>(null, 204, httpHeaders, null, null)); + + final Map tags = new HashMap<>(); + tags.put("owner", "test-user"); + + final Exchange exchange = new DefaultExchange(context); + exchange.getIn().setBody(tags); + + final BlobOperations operations = new BlobOperations(configuration, client); + final BlobOperationResponse response = operations.setBlobTags(exchange); + + assertNotNull(response); + assertTrue((boolean) response.getBody()); + } + + @Test + void testSetBlobTagsWithNoTagsThrows() { + final Exchange exchange = new DefaultExchange(context); + exchange.getIn().setBody("not-a-map"); + + final BlobOperations operations = new BlobOperations(configuration, client); + assertThrows(IllegalArgumentException.class, () -> operations.setBlobTags(exchange)); + } + + @SuppressWarnings("unchecked") + @Test + void testGetBlobTags() { + final Map tags = new HashMap<>(); + tags.put("status", "clean"); + tags.put("scannedBy", "antivirus"); + + final HttpHeaders httpHeaders = new HttpHeaders().set("x-test-header", "789"); + + when(client.getTags(any(), any())) + .thenReturn(new ResponseBase<>(null, 200, httpHeaders, tags, null)); + + final Exchange exchange = new DefaultExchange(context); + + final BlobOperations operations = new BlobOperations(configuration, client); + final BlobOperationResponse response = operations.getBlobTags(exchange); + + assertNotNull(response); + assertNotNull(response.getBody()); + final Map resultTags = (Map) response.getBody(); + assertEquals("clean", resultTags.get("status")); + assertEquals("antivirus", resultTags.get("scannedBy")); + assertEquals(tags, response.getHeaders().get(BlobConstants.BLOB_TAGS)); + assertEquals("789", ((HttpHeaders) response.getHeaders().get(BlobConstants.RAW_HTTP_HEADERS)) + .get("x-test-header").getValue()); + } + @Test void testCreateBlobSnapshot() { final String snapshotId = "2026-04-15T10:00:00.0000000Z"; diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BlobEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BlobEndpointBuilderFactory.java index 6501d349e9074..2c237fa247976 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BlobEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/BlobEndpointBuilderFactory.java @@ -4339,6 +4339,19 @@ public String azureStorageBlobContext() { public String azureStorageBlobSnapshotId() { return "CamelAzureStorageBlobSnapshotId"; } + /** + * (producer) (setBlobTags) The tags to set on the blob as key-value + * pairs. (consumer) The tags retrieved from the blob. + * + * The option is a: {@code Map} type. + * + * Group: common + * + * @return the name of the header {@code AzureStorageBlobTags}. + */ + public String azureStorageBlobTags() { + return "CamelAzureStorageBlobTags"; + } } static BlobEndpointBuilder endpointBuilder(String componentName, String path) { class BlobEndpointBuilderImpl extends AbstractEndpointBuilder implements BlobEndpointBuilder, AdvancedBlobEndpointBuilder {