Développer ma barre des réalisations de la Communauté.

Submissions are now open for the 2026 Adobe Experience Maker Awards.

Implement a custom Package Exporter for Sling Distribution

Avatar

Level 2

Requirement:

Currently, the Sling Distribution Forward Agents utilize the Apache Sling Distribution Packaging – Vault Package Builder Factory to export packages. However, I want to configure a custom Package Builder that can export or replicate files in their native format instead of using the Vault packaging mechanism.

In other words, the goal is to export the content as-is — for example:

  • If the file is a JSON, it should be exported as a JSON file.

  • If it’s an HTML file, export it as HTML.

  • If it’s an image, export it as an image.

At present, the default Vault packaging exports the data in a binary (vault) format, which is not desirable for this use case.

I would like to understand whether this can be achieved through configuration or if it requires a custom code implementation, so I can proceed accordingly.

ac4320_0-1761072203067.png
ac4320_1-1761072236963.png

ac4320_2-1761072283946.png

Currently using the default vault packaging in my forward agent :

ac4320_3-1761072346006.png

 

 

7 Replies

Avatar

Level 2

Do we have something like JSON package Builder? I was going through https://experienceleaguecommunities.adobe.com/t5/adobe-experience-manager/issue-is-sling-content-dis... which says 

  • Package builder mismatch (FileVault vs JSON).

So is there a JSON package builder available? If so - where is it and how to configure the same?

@VishalKa5 - Can you let me know if there's any such thing?

As of now I just want to focus on JSON only - any insights with the above use-case would be very helpful.


Thanks!

Avatar

Community Advisor

Hi @ac4320,

Short answer - No, Sling Distribution doesn’t include a JSON package builder. The default “Vault Package Builder Factory” always exports in FileVault format. If you want the content to be distributed as raw JSON, HTML, or binary files (instead of zipped JCR content), you’ll need to implement a custom DistributionPackageBuilder.

Here are the details:

  • The default builder PID is:
    org.apache.sling.distribution.serialization.impl.vlt.VaultDistributionPackageBuilderFactory

  • You can create your own builder by implementing the interface
    org.apache.sling.distribution.packaging.DistributionPackageBuilder.
    Inside your implementation, you can:

    • Read resources under the given paths (DistributionRequest)

    • Serialize them directly as JSON (using Sling’s ResourceResolver + JsonRenderer)

    • Write them to an output stream or a temporary file

  • Then register it as an OSGi component (e.g. name=json) and update your Forward Agent config to reference it:

     
    Package Builder: (name=json)

Example use case:
If your content under /content/data/*.json should be replicated as JSON files, your builder could simply export those nodes via Sling’s JSON exporter instead of creating a Vault package.

References:


Santosh Sai

AEM BlogsLinkedIn


Avatar

Level 2

@SantoshSai - Can you please help me with the code?

Avatar

Employee Advisor

Can you please elaborate on the usecase? Why do you want to have this? 

Avatar

Level 2

@Jörg_Hoh - We currently do not have an AEM Publisher instance in our setup. Instead, we are using a Sling Distribution Agent configured with an endpoint pointing to a servlet, which is responsible for posting the distributed data to a middleware service. For now, as part of a proof of concept (POC), we are saving the files locally on our machine. Once the middleware setup is complete, the data will be sent there accordingly.

At present, when using the out-of-the-box (OOTB) FileVault Package Exporter, the data is exported in a ZIP format, similar to the packages created via the AEM Package Manager.

However, our requirement is slightly different:
For instance, consider the folder /content/dam/fmdita-outputs, which contains subfolders such as child-map-1, parent-map, etc. Each of these subfolders includes JSON files. When we trigger Sling Distribution using our custom agent, we want it to export only the actual JSON files — maintaining the same folder structure — and save them directly in our local file system.

Currently, the default behavior creates a Vault package containing all node data, rather than exporting the original JSON files. Hence, we are exploring the possibility of implementing a custom Package Builder that can handle this logic and directly save or send the JSON files as-is.

I have attached the relevant code snippets from our setup along with the current output generated when using the FileVault Package Builder for reference.

Node structure:
The /content/dam/fmdita-outputs node represents the DAM folder that contains the generated output files. Each subfolder (e.g., child-map-1, parent-map, etc.) holds one or more JSON files — these are the actual output files that we want to replicate or export in their original format (not as part of a Vault package).

ac4320_0-1761487689406.png

 

Sling Distribution Agent Configuration (org.apache.sling.distribution.agent.impl.ForwardDistributionAgentFactory-json-exporter.cfg.json) :

{
  "name": "exporter",
  "enabled": true,
  "queue.processing.enabled": true,
  "allowed.roots": ["/content"],
  "packageImporter.endpoints": [
    "http://localhost:4502/bin/receiver"
  ],
  "requestAuthorizationStrategy.target": "(name=default)",
  "transportSecretProvider.target": "(name=default)"
}


Relevant servlet (configured in endpoint) :

package com.adobe.aem.common.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Component(
        service = Servlet.class,
        property = {
                "sling.servlet.methods=POST",
                "sling.servlet.paths=/bin/receiver"
        }
)
public class VaultBinaryExtractorServlet extends SlingAllMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(VaultBinaryExtractorServlet.class);

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {

        log.error("VaultBinaryExtractorServlet invoked!");
        String contentType = request.getContentType();
        log.error("Incoming Content-Type: " + contentType);

        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
        File rawFile = new File(System.getProperty("java.io.tmpdir"), "received_vault_data_" + timestamp + ".bin");
        File jsonFile = new File(System.getProperty("java.io.tmpdir"), "received_vault_data_" + timestamp + "_summary.json");

        log.error("Saving incoming stream to: " + rawFile.getAbsolutePath());

        try (InputStream inputStream = request.getInputStream();
             BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(rawFile))) {

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }

        log.error("Raw binary saved successfully: " + rawFile.length() + " bytes");

        try (FileInputStream fis = new FileInputStream(rawFile)) {
            byte[] headerBytes = new byte[16];
            fis.read(headerBytes);
            String header = new String(headerBytes, StandardCharsets.US_ASCII);
            log.error("Header signature: " + header);

            if (header.startsWith("DSTRPACKMETA")) {
                log.error("Detected Distribution Package stream. Extracting metadata and embedded ZIP...");
                extractReadableMetadata(rawFile, jsonFile);
                extractInnerZip(rawFile);
                log.error("Processing complete for file: " + rawFile.getName());
            } else {
                log.error("No DSTRPACKMETA header detected. Skipping extraction.");
            }

        } catch (Exception e) {
            log.error("Error analyzing binary header", e);
        }

        // Respond to caller
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType("application/json");
        String msg = "{\"message\":\"Binary file received and processed successfully.\"}";
        response.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8));
        response.getOutputStream().flush();
    }

    /**
     * Extracts ASCII/UTF-8 readable fragments and writes metadata summary as JSON.
     */
    private void extractReadableMetadata(File binFile, File jsonFile) {
        log.error("Starting lightweight binary-to-JSON conversion...");

        Map<String, Object> result = new LinkedHashMap<>();
        List<String> readableStrings = new ArrayList<>();

        try (InputStream in = new FileInputStream(binFile)) {
            byte[] data = in.readAllBytes();

            result.put("fileName", binFile.getName());
            result.put("fileSizeBytes", data.length);
            result.put("header", new String(data, 0, Math.min(16, data.length), StandardCharsets.US_ASCII));

            StringBuilder sb = new StringBuilder();
            for (byte b : data) {
                char c = (char) (b & 0xFF);
                if (Character.isISOControl(c) && c != '\n' && c != '\r') {
                    if (sb.length() >= 4) {
                        readableStrings.add(sb.toString());
                    }
                    sb.setLength(0);
                } else {
                    sb.append(c);
                }
            }
            if (sb.length() >= 4) readableStrings.add(sb.toString());

            List<String> probablePaths = readableStrings.stream()
                    .filter(s -> s.contains("/content/") || s.contains(".json") || s.contains("jcr_root"))
                    .collect(Collectors.toList());

            result.put("readableStringsFound", readableStrings.size());
            result.put("probablePaths", probablePaths);

            try (Writer writer = new FileWriter(jsonFile)) {
                writer.write(toPrettyJson(result));
            }

            log.error("Readable metadata extracted successfully: " + jsonFile.getAbsolutePath());

        } catch (Exception e) {
            log.error("Error extracting readable metadata", e);
        }
    }

    /**
     * Extracts the inner Vault ZIP stream from DSTRPACKMETA binary and writes it to disk.
     */
    private void extractInnerZip(File binFile) {
        log.error("Attempting to extract embedded ZIP from binary...");

        try (InputStream in = new FileInputStream(binFile)) {
            byte[] data = in.readAllBytes();

            int zipStart = -1;
            for (int i = 0; i < data.length - 1; i++) {
                if (data[i] == 'P' && data[i + 1] == 'K') {
                    zipStart = i;
                    break;
                }
            }

            if (zipStart == -1) {
                log.error("No ZIP signature (PK) found inside binary file!");
                return;
            }

            byte[] zipBytes = Arrays.copyOfRange(data, zipStart, data.length);

            File extractedZip = new File(System.getProperty("java.io.tmpdir"),
                    binFile.getName().replace(".bin", "_inner.zip"));
            try (FileOutputStream out = new FileOutputStream(extractedZip)) {
                out.write(zipBytes);
            }

            log.error("Inner ZIP extracted successfully: " + extractedZip.getAbsolutePath());
            log.error("You can now unzip this to see jcr_root/... JSON and XML files.");

        } catch (Exception e) {
            log.error("Error extracting inner ZIP from DSTRPACKMETA", e);
        }
    }

    /**
     * Simple pretty JSON formatter (no external libs).
     */
    private String toPrettyJson(Map<String, Object> map) {
        StringBuilder sb = new StringBuilder("{\n");
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            sb.append("  \"").append(entry.getKey()).append("\": ");
            Object value = entry.getValue();
            if (value instanceof String) {
                sb.append("\"").append(value.toString().replace("\"", "\\\"")).append("\"");
            } else if (value instanceof Collection) {
                sb.append("[\n");
                for (Object item : (Collection<?>) value) {
                    sb.append("    \"").append(item.toString().replace("\"", "\\\"")).append("\",\n");
                }
                if (sb.charAt(sb.length() - 2) == ',') sb.setLength(sb.length() - 2);
                sb.append("\n  ]");
            } else {
                sb.append(value);
            }
            sb.append(",\n");
        }
        if (sb.charAt(sb.length() - 2) == ',') sb.setLength(sb.length() - 2);
        sb.append("\n}");
        return sb.toString();
    }
}


Output (with FileVault Package Builder)https://drive.google.com/file/d/1eILYNIP15cQE44cRw_uIkFSazWn_qpw8/view?usp=sharing 

If it’s possible to continue using the FileVault Package Builder and achieve the desired behavior — i.e., only post the JSON files — through certain tweaks or configurations in the existing implementation, we are open to exploring that approach as well. Please let me know if this can be done.

Avatar

Employee Advisor

I wonder if the replication mechanism is the right approach to your requirement; replication was built in the first place to get a copy of content from the local instance to a remote one. Technically it can used that way you want, but then you would a custom ContentBuilder and a custom transport handler, which you can configure in the replication agent (assuming this AEM 6.5 ... not sure if/how that works in CS).

 

 

 

 

 

Avatar

Level 2

Since we’re on AEMaaCS, we’re specifically looking for AEMaaCS-compliant solutions.