Integrate AEM with Azure Storage SDK

Let’s see how to integrate AEM with Azure SDK for uploading AEM assets to Azure blob storage. Recently i worked on a very interesting use case, where we need to move old/deprecated assets that are no longer i use to a separate storage on Azure and remove them from AEM.

In order to achieve this we need to first integrate AEM with Azure SDK and then write a custom servlet to the job. The major challenge that i have faced is adding dependencies for Azure SDK in our pom.xml on AEM as Cloud Environment.

So, In this tutorial we will cover:-

  • How to integrate AEM with Azure Storage SDK.
  • Sample code for uploading Asset from AEM to Azure blob storage along with custom Metadata.
  • Troubleshooting and limitations

Integrate AEM with Azure Storage SDK:-

Azure SDK is modular and very well documented. In this tutorial i am using aem-sdk-2025.3.19823.20250304T101418Z-250200 cloud version and Azure SDK 1.2.23 version for integration.

Let’s start with integration :-

In our parent pom.xml add the following dependency:-

<dependency>
	<groupId>com.azure</groupId>
	<artifactId>azure-sdk-bom</artifactId>
	<version>1.2.23</version>
	<type>pom</type>
	<scope>import</scope>
</dependency>

Now, navigate to core pom.xml and add following dependencies:-

        <dependency>
            <groupId>com.azure</groupId>
            <artifactId>azure-core-http-okhttp</artifactId>
        </dependency>
        <dependency>
            <groupId>com.azure</groupId>
            <artifactId>azure-storage-blob</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.azure</groupId>
                    <artifactId>azure-core-http-netty</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Note:- One important thing to note here is, i have excluded azure-core-http-netty bundle, Azure uses azure-core-http-netty or azure-core-http-okhttp as default bundle for making calls to Azure, but there are too many inter dependencies required for adding azure-core-http-netty, thaat’s why i went with azure-core-http-okhttp.

And finally, update the bnd-maven-plugin configuration in your core pom.xml to include the resources needed by the Azure SDK clients.

This is the place that ate most of my time and gave me a headache. Because in recent AEM versions, AEM is using AEM Analyser to verify all the dependencies and its import/export configuration , so that it will get successfully deployed on AEM as Cloud environments. If you have not updated all the dependencies required by your bundles here, then it will keep on throwing below errors during build time.

[ERROR] The analyser found the following errors for author and publish : 
[ERROR] [api-regions-exportsimports] com.act:aemcq5tutorials.core:1.0.0-SNAPSHOT: Bundle aemcq5tutorials.core:1.0.0-SNAPSHOT is importing package(s) [com.azure.core.util, com.azure.storage.blob.options, com.azure.core.http.rest, com.azure.storage.blob] in start level 20 but no bundle is exporting these for that start level. (com.act:aemcq5tutorials.all:1.0.0-SNAPSHOT)

Update your bnd-maven-plugin configuration in your core pom.xml as shown below:-

<plugin>
    <groupId>biz.aQute.bnd</groupId>
     <artifactId>bnd-maven-plugin</artifactId>
         <executions>
             <execution>
                  <id>bnd-process</id>
                       <goals>
                           <goal>bnd-process</goal>
                       </goals>
  <configuration>
    <bnd><![CDATA[
Import-Package: javax.annotation;version=0.0.0, \
                !sun.misc, \
                !com.sun.*, \
                !org.bouncycastle.*, \
                !org.opensaml.*, \
                !com.azure.identity.broker.*, \
                !com.google.crypto.tink.*, \
                !jakarta.servlet.*, \
                !net.jcip.annotations, \
                !net.shibboleth.utilities.java.support.xml, \
                !org.cryptomator.siv, \
                !reactor.blockhound.*, \
                !javax.annotation.meta, \
                !io.micrometer.core.instrument.*, \
                !org.objectweb.asm, \
                !sun.security.ssl, \
                !android.*, \
                !dalvik.system, \
                !org.conscrypt, \
                !org.openjsse.*, \
                !kotlin.reflect.jvm.internal, \
                *
-includeresource azure-*-*.jar;lib:=true, \
                 reactive-streams-*.jar;lib:=true, \
                 reactor-core-*.jar;lib:=true, \
                 okhttp-*.jar;lib:=true, \
                 okio-*.jar;lib:=true, \
                 @kotlin-*.jar!/!*/module-info.class
                                ]]></bnd>
                   </configuration>
               </execution>
           </executions>
  </plugin>

That’s it. We have now successfully integrated our AEM instance with Azure Storage Blob SDK.

Sample code for uploading Asset from AEM to Azure blob storage with Custom Metadata

Below is my test servlet that i have created for uploading AEM asset to Azure blob storage.

package com.aemcq5tutorials.core.servlets;

import com.azure.core.http.rest.Response;
import com.azure.core.util.Context;
import com.azure.storage.blob.BlobClient;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.HashMap;

import javax.servlet.Servlet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

@SuppressWarnings("CQRules:CQBP-75")
@Component(service = Servlet.class,
        property = {
                "sling.servlet.methods=" + HttpConstants.METHOD_GET,
                "sling.servlet.paths=/bin/OffloadAssetsToAzure"
        })
public class OffloadAssetsToAzureServlet extends SlingAllMethodsServlet {

    private static final Logger logger = LoggerFactory.getLogger(OffloadAssetsToAzureServlet.class);

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response){
     try{
         String assetPath = request.getParameter("assetPath") != null ?request.getParameter("assetPath") : StringUtils.EMPTY;
         logger.info("Entering OffloadAssetsToAzureServlet");
         ResourceResolver resourceResolver = request.getResourceResolver();
         BlobServiceClient blobServiceClient = new BlobServiceClientBuilder()
                 .endpoint("https://aemcq5tutorialsteststorage.blob.core.windows.net")
                 .connectionString("<Enter your connection String here>")
                 .buildClient();
         BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient("expired-assets-dev");
         if(StringUtils.isNotEmpty(assetPath)){
             uploadAssetToAzure(blobContainerClient, assetPath, resourceResolver);
         }
         response.setContentType("application/json");
        response.getWriter().write("Azure Sample Asset upload call completed");
        response.getWriter().close();
    } catch (Exception e) {
        logger.error("Error processing spreadsheet.", e);
    }
    }

    private void uploadAssetToAzure(BlobContainerClient blobContainerClient, String assetPath, ResourceResolver resourceResolver) {

        BlobClient blobClient = blobContainerClient.getBlobClient(assetPath.substring(1));
        Resource assetRes = resourceResolver.getResource(assetPath+"/jcr:content/renditions/original");

        if(assetRes == null){
            return;
        }

        /*
         * If the max overload is needed and no access conditions are passed, the upload will succeed as both a
         * create and overwrite.
         */
        try {
            Map<String, String> assetMetadata = new HashMap<String, String>();
            assetMetadata.put("docType1", "text");
            assetMetadata.put("category1", "reference");

            ByteArrayInputStream inputDataStream = convertResourceToByteArrayInputStream(assetRes);
            BlobParallelUploadOptions options =
                    new BlobParallelUploadOptions(inputDataStream).setMetadata(assetMetadata);
           Response blobResponse = blobClient.uploadWithResponse(options, null, Context.NONE);
            int responseCode = blobResponse.getStatusCode();
            if(responseCode == 200){
                logger.info("Asset is successfully uploaded to Azure");
            } else{
                logger.error("Asset upload failed");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ByteArrayInputStream convertResourceToByteArrayInputStream(Resource resource) throws IOException {
        InputStream inputStream = null;
        ByteArrayInputStream byteArrayInputStream;
        try {
            inputStream = resource.adaptTo(InputStream.class);
            byte[] byteArray = IOUtils.toByteArray(inputStream);
            byteArrayInputStream = new ByteArrayInputStream(byteArray);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
        return byteArrayInputStream;
    }

}

once code is deployed to local AEM instance, you can hit below URL to test your AEM Azure SDK integration.

http://localhost:4502/bin/OffloadAssetsToAzure?assetPath=/content/dam/library/aemcq5tutorials/file_example_JPG_100kB.jpg

Now navigate to your Azure Storage container to verify the upload.

Note:- I had a requirement to keep same folder hierarchy in Azure like it is there in AEM that’s why on line no. 63 , I am passing full asset path. If you need to keep only asset without folder hierarchy you can simply pass just Asset name.

Troubleshooting and limitations:-

  • As per Azure , there is a max limit on the metadata that can be sent with each blob and that is 8KB. If you try to send metadata size greater than 8KB, then you can see below error in your log file.
com.azure.storage.blob.models.BlobStorageException: Status code 400, "
<?xml version="1.0" encoding="utf-8"?><Error><Code>MetadataTooLarge
</Code><Message>The metadata specified exceeds the maximum 
permissible limit (8KB)._RequestId:f98d9040-901e-0005-5930-c4c010000000_Time:
2025-05-13T17:59:23.4940577Z</Message></Error>"
  • While converting AEM metadata object value for example date or any other non string field type into string type. If it is not converted correctly and passed into the Azure upload call then below error will be thrown.
12.05.2025 08:55:38.638 *ERROR* [OkHttp https://aemcq5tutorialsstorage.blob.core.windows.net/...] reactor.core.publisher.Operators Operator called default onErrorDropped
java.lang.LinkageError: Package versions: jackson-core=2.15.2, jackson-databind=2.15.2, jackson-dataformat-xml=2.15.2, jackson-datatype-jsr310=2.15.2, azure-core=1.48.0, Troubleshooting version conflicts: https://aka.ms/azsdk/java/dependency/troubleshoot
	at com.azure.core.implementation.jackson.ObjectMapperShim.createXmlMapper(ObjectMapperShim.java:84) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.util.serializer.JacksonAdapter$GlobalXmlMapper.<clinit>(JacksonAdapter.java:66) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.util.serializer.JacksonAdapter.getXmlMapper(JacksonAdapter.java:467) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.util.serializer.JacksonAdapter.lambda$deserialize$9(JacksonAdapter.java:349) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.util.serializer.JacksonAdapter.useAccessHelper(JacksonAdapter.java:488) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.util.serializer.JacksonAdapter.deserialize(JacksonAdapter.java:344) [wedam.core:1.0.0.SNAPSHOT]
	at com.azure.core.implementation.serializer.HttpResponseBodyDecoder.deserialize(HttpResponseBodyDecoder.java:176) [wedam.core:1.0.0.SNAPSHOT]
  • Only String metadata value can be sent and key (AEM node name), can’t have any special characters. So, you need to apply some logic to either replace special char with some sequence like _ by UUUU. So that it can be decoded back.
Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.