diff --git a/README.adoc b/README.adoc
index b59988ad3..9aada0383 100644
--- a/README.adoc
+++ b/README.adoc
@@ -27,7 +27,7 @@ readme's instructions.
=== Examples
// examples: START
-Number of Examples: 72 (0 deprecated)
+Number of Examples: 73 (0 deprecated)
[width="100%",cols="4,2,4",options="header"]
|===
@@ -81,6 +81,8 @@ Number of Examples: 72 (0 deprecated)
| link:azure/camel-example-spring-boot-azure-servicebus/README.adoc[Spring Boot Azure Servicebus] (camel-example-spring-boot-azure-servicebus) | Cloud | An example showing how to work with Camel, Azure Service Bus and Spring Boot
+| link:azure/camel-example-spring-boot-azure-storage-blob/README.adoc[Spring Boot Azure Storage Blob] (camel-example-spring-boot-azure-storage-blob) | Cloud | An example showing how to work with Camel, Azure Storage Blob and Spring Boot
+
| link:infinispan/README.adoc[Infinispan] (infinispan) | Cloud | An example showing the Camel Infinispan component with Spring Boot
| link:webhook/readme.adoc[Webhook] (webhook) | Cloud | Example on how to use the Camel Webhook component
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/README.adoc b/azure/camel-example-spring-boot-azure-storage-blob/README.adoc
new file mode 100644
index 000000000..02a401bab
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/README.adoc
@@ -0,0 +1,37 @@
+== Camel Azure Storage Blob Example
+
+=== Introduction
+
+This example demonstrates how to use the Apache Camel Azure Storage Blob component with Spring Boot to:
+
+* Upload block blobs
+* Set and get blob index tags
+* Find blobs by tag filter expressions
+* Create and read blob snapshots
+
+=== Running Tests (Azurite)
+
+Tests run automatically against https://github.com/Azure/Azurite[Azurite] (Azure Storage emulator) via Testcontainers. Docker must be running.
+
+ mvn verify
+
+=== Running with Azure
+
+To run against a real Azure Storage account:
+
+1. Create a General-purpose v2 Storage Account in the Azure portal
+2. Enable blob versioning under Data management > Data protection (optional, for version operations)
+3. Get your account name and access key from Security + networking > Access keys
+
+ export AZURE_STORAGE_ACCOUNT_NAME=youraccount
+ export AZURE_STORAGE_ACCESS_KEY=yourkey
+ export AZURE_STORAGE_CONTAINER_NAME=yourcontainer
+ mvn spring-boot:run
+
+=== Help and Contributions
+
+If you hit any problems please let us know on the https://camel.apache.org/community/mailing-list/[Camel Forums].
+
+Please help us make Apache Camel better - we appreciate any feedback you may have.
+
+https://camel.apache.org/community/contributing/[Contributing]
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/pom.xml b/azure/camel-example-spring-boot-azure-storage-blob/pom.xml
new file mode 100644
index 000000000..915f9f569
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/pom.xml
@@ -0,0 +1,102 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.camel.springboot.example
+ camel-example-spring-boot-azure
+ 4.23.0-SNAPSHOT
+
+
+ camel-example-spring-boot-azure-storage-blob
+ Camel SB Examples :: Azure Storage Blob
+ An example showing how to work with Camel, Azure Storage Blob and Spring Boot
+
+
+ Cloud
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.apache.camel.springboot
+ camel-spring-boot-starter
+
+
+ org.apache.camel.springboot
+ camel-azure-storage-blob-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.apache.camel
+ camel-test-spring-junit6
+ test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers-version}
+ test
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ ${testcontainers-version}
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot-version}
+
+
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java
new file mode 100644
index 000000000..db96c3789
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java
@@ -0,0 +1,44 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class BlobConfiguration {
+
+ @Value("${azure.storage.account-name}")
+ private String accountName;
+
+ @Value("${azure.storage.access-key}")
+ private String accessKey;
+
+ @Bean
+ BlobServiceClient serviceClient() {
+ String endpoint = String.format("https://%s.blob.core.windows.net", accountName);
+ return new BlobServiceClientBuilder()
+ .endpoint(endpoint)
+ .credential(new StorageSharedKeyCredential(accountName, accessKey))
+ .buildClient();
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java
new file mode 100644
index 000000000..34c2e6385
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobRouteBuilder extends RouteBuilder {
+
+ @Override
+ public void configure() {
+ from("direct:uploadBlob")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=uploadBlockBlob"
+ + "&serviceClient=#serviceClient");
+
+ from("direct:getBlob")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=getBlob"
+ + "&serviceClient=#serviceClient")
+ .convertBodyTo(byte[].class);
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java
new file mode 100644
index 000000000..7dd4b7d21
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobSnapshotRouteBuilder extends RouteBuilder {
+
+ @Override
+ public void configure() {
+ from("direct:createSnapshot")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=createBlobSnapshot"
+ + "&serviceClient=#serviceClient");
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java
new file mode 100644
index 000000000..b23df7aaf
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class BlobStorageApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BlobStorageApplication.class, args);
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java
new file mode 100644
index 000000000..a55fc40e2
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobTagsRouteBuilder extends RouteBuilder {
+
+ @Override
+ public void configure() {
+ from("direct:setBlobTags")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=setBlobTags"
+ + "&serviceClient=#serviceClient");
+
+ from("direct:getBlobTags")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=getBlobTags"
+ + "&serviceClient=#serviceClient");
+
+ from("direct:findBlobsByTags")
+ .to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+ + "?operation=findBlobsByTags"
+ + "&serviceClient=#serviceClient");
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml b/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml
new file mode 100644
index 000000000..27d640bdc
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+azure:
+ storage:
+ account-name: ${AZURE_STORAGE_ACCOUNT_NAME:devstoreaccount1}
+ access-key: ${AZURE_STORAGE_ACCESS_KEY}
+ container-name: ${AZURE_STORAGE_CONTAINER_NAME:testcontainer}
+
+camel:
+ component:
+ azure-storage-blob:
+ credential-type: SHARED_KEY_CREDENTIAL
+ springboot:
+ main-run-controller: true
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java
new file mode 100644
index 000000000..dd835c8eb
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+ CamelAutoConfiguration.class,
+ BlobSmokeTest.class,
+ BlobSmokeTest.TestConfiguration.class,
+ BlobRouteBuilder.class
+})
+public class BlobSmokeTest {
+
+ static final String ACCOUNT_NAME = "devstoreaccount1";
+ static final String ACCOUNT_KEY =
+ "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+ static final String CONTAINER_NAME = "smoketest";
+
+ static GenericContainer> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+ .withExposedPorts(10000)
+ .waitingFor(Wait.forListeningPort());
+
+ static {
+ azurite.start();
+ }
+
+ @DynamicPropertySource
+ static void azuriteProperties(DynamicPropertyRegistry registry) {
+ registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+ registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+ registry.add("camel.component.azure-storage-blob.credential-type", () -> "SHARED_KEY_CREDENTIAL");
+ }
+
+ @Autowired
+ CamelContext context;
+
+ @Autowired
+ ProducerTemplate template;
+
+ @Test
+ void testAzuriteReachable() throws Exception {
+ String url = String.format("http://%s:%d/devstoreaccount1?comp=list",
+ azurite.getHost(), azurite.getMappedPort(10000));
+ HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
+ conn.setRequestMethod("GET");
+ int status = conn.getResponseCode();
+ assertTrue(status == 200 || status == 403, "Azurite should be reachable, got " + status);
+ }
+
+ @Test
+ void testServiceClientInRegistry() {
+ BlobServiceClient client = context.getRegistry().lookupByNameAndType("serviceClient", BlobServiceClient.class);
+ assertNotNull(client, "serviceClient should be in the Camel registry");
+ }
+
+ @Test
+ void testUploadAndDownloadBlob() {
+ BlobServiceClient client = context.getRegistry().lookupByNameAndType("serviceClient", BlobServiceClient.class);
+ client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+ String expected = "hello azurite";
+ template.send("direct:uploadBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, "smoke.txt");
+ exchange.getIn().setBody(new ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8)));
+ });
+
+ Exchange result = template.send("direct:getBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, "smoke.txt");
+ });
+ assertNull(result.getException(), "Download should succeed: " + result.getException());
+ assertEquals(expected, result.getMessage().getBody(String.class));
+ }
+
+ @Configuration
+ public static class TestConfiguration {
+ @Bean
+ BlobServiceClient serviceClient() {
+ String endpoint = String.format("http://%s:%d/%s",
+ azurite.getHost(), azurite.getMappedPort(10000), ACCOUNT_NAME);
+ return new BlobServiceClientBuilder()
+ .endpoint(endpoint)
+ .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, ACCOUNT_KEY))
+ .buildClient();
+ }
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java
new file mode 100644
index 000000000..dc6efdf92
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+ CamelAutoConfiguration.class,
+ BlobSnapshotTest.class,
+ BlobSnapshotTest.AzuriteConfiguration.class,
+ BlobRouteBuilder.class,
+ BlobSnapshotRouteBuilder.class
+})
+public class BlobSnapshotTest {
+
+ static final String ACCOUNT_NAME = "devstoreaccount1";
+ static final String ACCOUNT_KEY =
+ "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+ static final String CONTAINER_NAME = "snapshottest";
+ static final String BLOB_NAME = "snapshot-blob.txt";
+ static final String ORIGINAL_CONTENT = "original snapshot content";
+
+ static GenericContainer> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+ .withExposedPorts(10000)
+ .waitingFor(Wait.forListeningPort());
+
+ static {
+ azurite.start();
+ }
+
+ @DynamicPropertySource
+ static void azuriteProperties(DynamicPropertyRegistry registry) {
+ registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+ registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+ registry.add("camel.component.azure-storage-blob.credential-type", () -> "SHARED_KEY_CREDENTIAL");
+ }
+
+ @Configuration
+ static class AzuriteConfiguration {
+ @Bean
+ BlobServiceClient serviceClient() {
+ String endpoint = String.format("http://%s:%d/%s",
+ azurite.getHost(), azurite.getMappedPort(10000), ACCOUNT_NAME);
+ return new BlobServiceClientBuilder()
+ .endpoint(endpoint)
+ .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, ACCOUNT_KEY))
+ .buildClient();
+ }
+ }
+
+ @Autowired
+ CamelContext camelContext;
+
+ @Autowired
+ ProducerTemplate template;
+
+ void uploadTestBlob() {
+ BlobServiceClient client = camelContext.getRegistry().lookupByNameAndType("serviceClient", BlobServiceClient.class);
+ client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+ byte[] content = ORIGINAL_CONTENT.getBytes(StandardCharsets.UTF_8);
+ template.send("direct:uploadBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ exchange.getIn().setBody(new ByteArrayInputStream(content));
+ });
+ }
+
+ @Test
+ void testCreateSnapshotReturnsId() {
+ uploadTestBlob();
+
+ Exchange result = template.send("direct:createSnapshot", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ });
+ assertNull(result.getException(), "createSnapshot should succeed: " + result.getException());
+
+ String snapshotId = result.getMessage().getHeader(BlobConstants.BLOB_SNAPSHOT_ID, String.class);
+ assertNotNull(snapshotId, "Snapshot ID should be returned in header");
+ }
+
+ @Test
+ void testReadBlobViaSnapshotId() {
+ uploadTestBlob();
+
+ Exchange snapshotResult = template.send("direct:createSnapshot", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ });
+ String snapshotId = snapshotResult.getMessage().getHeader(BlobConstants.BLOB_SNAPSHOT_ID, String.class);
+ assertNotNull(snapshotId);
+
+ String updatedContent = "modified after snapshot";
+ template.send("direct:uploadBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ exchange.getIn().setBody(new ByteArrayInputStream(updatedContent.getBytes(StandardCharsets.UTF_8)));
+ });
+
+ Exchange snapshotRead = template.send("direct:getBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ exchange.getIn().setHeader(BlobConstants.BLOB_SNAPSHOT_ID, snapshotId);
+ });
+ assertNull(snapshotRead.getException(), "Snapshot read should succeed: " + snapshotRead.getException());
+ String retrieved = snapshotRead.getMessage().getBody(String.class);
+ assertEquals(ORIGINAL_CONTENT, retrieved, "Snapshot should return original content");
+
+ Exchange liveRead = template.send("direct:getBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ });
+ assertNull(liveRead.getException(), "Live read should succeed: " + liveRead.getException());
+ String liveRetrieved = liveRead.getMessage().getBody(String.class);
+ assertEquals(updatedContent, liveRetrieved, "Live blob should return updated content");
+ }
+}
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java
new file mode 100644
index 000000000..4a8142494
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.
+ */
+package camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+ CamelAutoConfiguration.class,
+ BlobTagsTest.class,
+ BlobTagsTest.AzuriteConfiguration.class,
+ BlobRouteBuilder.class,
+ BlobTagsRouteBuilder.class
+})
+public class BlobTagsTest {
+
+ static final String ACCOUNT_NAME = "devstoreaccount1";
+ static final String ACCOUNT_KEY =
+ "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+ static final String CONTAINER_NAME = "tagtest";
+ static final String BLOB_NAME = "test-blob.txt";
+
+ static GenericContainer> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+ .withExposedPorts(10000)
+ .waitingFor(Wait.forListeningPort());
+
+ static {
+ azurite.start();
+ }
+
+ @DynamicPropertySource
+ static void azuriteProperties(DynamicPropertyRegistry registry) {
+ registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+ registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+ registry.add("camel.component.azure-storage-blob.credential-type", () -> "SHARED_KEY_CREDENTIAL");
+ }
+
+ @Configuration
+ static class AzuriteConfiguration {
+ @Bean
+ BlobServiceClient serviceClient() {
+ String endpoint = String.format("http://%s:%d/%s",
+ azurite.getHost(), azurite.getMappedPort(10000), ACCOUNT_NAME);
+ return new BlobServiceClientBuilder()
+ .endpoint(endpoint)
+ .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, ACCOUNT_KEY))
+ .buildClient();
+ }
+ }
+
+ @Autowired
+ CamelContext camelContext;
+
+ @Autowired
+ ProducerTemplate template;
+
+ void uploadTestBlob() {
+ BlobServiceClient client = camelContext.getRegistry().lookupByNameAndType("serviceClient", BlobServiceClient.class);
+ client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+ byte[] content = "hello blob tags".getBytes(StandardCharsets.UTF_8);
+ template.send("direct:uploadBlob", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ exchange.getIn().setBody(new ByteArrayInputStream(content));
+ });
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ void testSetAndGetBlobTags() {
+ uploadTestBlob();
+
+ Map tags = Map.of(
+ "status", "quarantine",
+ "category", "document",
+ "priority", "high");
+
+ Exchange setResult = template.send("direct:setBlobTags", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ exchange.getIn().setHeader(BlobConstants.BLOB_TAGS, tags);
+ });
+ assertNull(setResult.getException(), "setBlobTags should succeed: " + setResult.getException());
+
+ Exchange result = template.send("direct:getBlobTags", exchange -> {
+ exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+ });
+ assertNull(result.getException(), "getBlobTags should succeed: " + result.getException());
+
+ Map retrieved = result.getMessage().getBody(Map.class);
+ assertNotNull(retrieved, "Tags should not be null");
+ assertEquals("quarantine", retrieved.get("status"));
+ assertEquals("document", retrieved.get("category"));
+ assertEquals("high", retrieved.get("priority"));
+ }
+}
diff --git a/azure/pom.xml b/azure/pom.xml
index f03b0c4d0..b88ba6614 100644
--- a/azure/pom.xml
+++ b/azure/pom.xml
@@ -63,6 +63,7 @@
camel-example-spring-boot-azure-servicebus
camel-example-spring-boot-azure-eventhubs
+ camel-example-spring-boot-azure-storage-blob