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