AEM Tool – Access CRX Package manager in AEM PROD

Problem statement:

How to access the CRX package manager in AEM PROD or AEM as Cloud services?

User cases:

  1. Latest content package from PROD to lowers or local for debugging purposes
  2. Install the content package on PROD
  3. Continue the PROD deployment during CM outage in between deployment

Introduction:

AEM OOTB comes with multiple tools in AEM and to access all the tools you need to navigate to the tool section and select the appropriate sections to perform all the operations

For example:

  1. AEM operations
  2. Managing templates
  3. Cloud configurations
  4. ACS Commons tools etc..

Tools are an essential part of AEM and avoid any dependency on Groovy scripts or any add scripts and can be managed or extended at any given point in time.

Package handler can be used to upload, install, build or delete packages and we are using JCR Package manager to achieve all the above options.

Usually, if want to perform any operations on AEM as managed services are AEMaaCS we need to go through CAB, and AMS resources will perform all the operations as mentioned on the CAB. However, if your project has shared resources then all the priority package operations will take more time on PROD or any other environments.

In order to create a perfect content backup package please use the Tool generator to give your tool name and descriptions: https://kiransg.com/2022/11/24/aem-tool-create-generate-tool-from-scratch

Add the following Sling model fields as shown below to accept the inputs:

package com.aem.operations.core.models;

import java.io.InputStream;
import javax.inject.Named;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.form.GeneratedDialog;
import com.adobe.acs.commons.mcp.form.RadioComponent;
import lombok.Getter;
import lombok.Setter;

@Model(adaptables = { Resource.class,SlingHttpServletRequest.class }, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class PackageHandlerModel extends GeneratedDialog {

    @Getter
    @Setter
    @Named(value = "inputPackage")
    @FormField(name = "Package", description = "Upload JCR Package", component = FileUploadComponent.class)
    private InputStream inputPackage = null;

    @Getter
    @Setter
    @Named(value = "packageName")
    @FormField(
            name = "Package Name",
            category = "General",
            required = false,
            description = "Enter the package name",
            hint = "Name")
    private String packageName;

    @Getter
    @Setter
    @Named(value = "packageGroup")
    @FormField(
            name = "Package Group",
            category = "General",
            required = false,
            description = "Enter the package group",
            hint = "Group")
    private String packageGroup;

    @Getter
    @Setter
    @Named(value = "packageVersion")
    @FormField(
            name = "Package Version",
            category = "General",
            required = false,
            description = "Enter the package version",
            hint = "Version")
    private String packageVersion;

    @Getter
    @Setter
    @Named(value = "packageOperation")
    @FormField(
            name = "Package Operation",
            description = "Select the operation to be performed",
            required = true,
            component = RadioComponent.EnumerationSelector.class,
            options = {"horizontal", "default=UPLOAD"})
    private Mode packageOperation;

    public enum Mode {
        UPLOAD, UPLOAD_INSTALL, BUILD, INSTALL, DELETE
    }
}

Add the following Servlet to process the request as shown below:

package com.aem.operations.core.servlets;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import com.aem.operations.core.models.PackageHandlerModel;
import com.aem.operations.core.services.PackageHandlerService;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.servlets.post.JSONResponse;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.adobe.granite.rest.Constants;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

@Component(
        service = { Servlet.class },
        property = {
                "sling.servlet.paths=" + PackageHandlerServlet.RESOURCE_PATH,
                "sling.servlet.methods=POST"
        }
)
public class PackageHandlerServlet extends SlingAllMethodsServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger LOGGER = LoggerFactory.getLogger(PackageHandlerServlet.class);
    public static final String RESOURCE_PATH = "/bin/triggerPackageHandler";
    private static final String MESSAGE = "message";

    @Reference
    private PackageHandlerService packageHandlerService;

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        // Set response headers.
        response.setContentType(JSONResponse.RESPONSE_CONTENT_TYPE);
        response.setCharacterEncoding(Constants.DEFAULT_CHARSET);

        JsonObject jsonResponse = new JsonObject();
        ResourceResolver resourceResolver = request.getResourceResolver();

        String packageName = request.getParameter("packageName");
        String packageGroup = request.getParameter("packageGroup");
        String packageVersion = request.getParameter("packageVersion");
        String packageOperation = request.getParameter("packageOperation");
        @Nullable RequestParameter inputPackStream = request.getRequestParameter("file");

        String packagePath = StringUtils.EMPTY;
        if (StringUtils.equalsIgnoreCase(PackageHandlerModel.Mode.UPLOAD.toString(), packageOperation)) {
            packagePath = packageHandlerService.uploadPack(resourceResolver, inputPackStream);
        } else  if(StringUtils.equalsIgnoreCase(PackageHandlerModel.Mode.BUILD.toString(), packageOperation)) {
            packagePath = packageHandlerService.buildPackage(resourceResolver, packageGroup, packageName, packageVersion);
        } else if(StringUtils.equalsIgnoreCase(PackageHandlerModel.Mode.UPLOAD_INSTALL.toString(), packageOperation)) {
            packagePath = packageHandlerService.uploadAndInstallPack(resourceResolver, inputPackStream);
        } else if(StringUtils.equalsIgnoreCase(PackageHandlerModel.Mode.INSTALL.toString(), packageOperation)) {
            packagePath = packageHandlerService.installPackage(resourceResolver, packageGroup, packageName, packageVersion, ImportMode.REPLACE, AccessControlHandling.IGNORE);
        } else if(StringUtils.equalsIgnoreCase(PackageHandlerModel.Mode.DELETE.toString(), packageOperation)) {
            packagePath = packageHandlerService.deletePackage(resourceResolver, packageGroup, packageName, packageVersion);
        }
        if(StringUtils.isNotEmpty(packagePath)) {
            jsonResponse.addProperty(MESSAGE, "Package Operation is Successful and path is <a href=\"" + packagePath + "\">" + packagePath + "</a>");
        } else {
            LOGGER.error("Unable to process package operation");
        }
        try (PrintWriter out = response.getWriter()) {
            out.print(new Gson().toJson(jsonResponse));
        }
    }
}

Add the following Package service as shown below:

package com.aem.operations.core.services;

import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.jetbrains.annotations.Nullable;

public interface PackageHandlerService {

	
	/**
	 * @param resourceResolver
	 * @param inputPackage
	 * @return package Path
	 */
	public String uploadPack(ResourceResolver resourceResolver, @Nullable RequestParameter inputPackage);

	/**
	 * @param resourceResolver
	 * @param groupName
	 * @param packageName
	 * @param version
	 * @return
	 */
	public String buildPackage(ResourceResolver resourceResolver, final String groupName, final String packageName,
			final String version);

	/**
	 * @param resourceResolver
	 * @param groupName
	 * @param packageName
	 * @param version
	 * @param importMode
	 * @param aclHandling
	 * @return package Path
	 */
	public String installPackage(ResourceResolver resourceResolver, final String groupName, final String packageName,
			final String version, final ImportMode importMode, final AccessControlHandling aclHandling);

	/**
	 * @param resourceResolver
	 * @param inputPackage
	 * @return package Path
	 */
	public String uploadAndInstallPack(ResourceResolver resourceResolver, @Nullable RequestParameter inputPackage);

	/**
	 * @param resourceResolver
	 * @param groupName
	 * @param packageName
	 * @param version
	 * @return package Path
	 */
	public String deletePackage(ResourceResolver resourceResolver, final String groupName, final String packageName,
			final String version);
}

Add the following Package service Implementation as shown below:

package com.aem.operations.core.services.impl;

import com.aem.operations.core.services.PackageHandlerService;
import com.aem.operations.core.utils.VltUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.packaging.*;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.IOException;
import java.io.InputStream;

@Component(service = PackageHandlerService.class, immediate = true, name = "Package Handler Service")
public class PackageHandlerServiceImpl implements PackageHandlerService {

	private static final Logger LOGGER = LoggerFactory.getLogger(PackageHandlerServiceImpl.class);

	@Reference
	private Packaging packaging;

	@Override
	public String uploadPack(ResourceResolver resourceResolver, @Nullable RequestParameter inputPackage) {
		if (null != inputPackage) {
			try (InputStream inputPack = inputPackage.getInputStream()) {
				if (null != inputPack) {
					final JcrPackageManager packageManager = packaging
							.getPackageManager(resourceResolver.adaptTo(Session.class));
					JcrPackage jcrPackage = packageManager.upload(inputPack, true);
					return jcrPackage.getNode().getPath();
				}
			} catch (IOException | RepositoryException e) {
				LOGGER.error("Could not upload package {}", e.getMessage());
				return StringUtils.EMPTY;
			}
		}
		return StringUtils.EMPTY;
	}

	@Override
	public String buildPackage(ResourceResolver resourceResolver, String groupName, String packageName,
			String version) {
		if (StringUtils.isNotEmpty(packageName) && StringUtils.isNotEmpty(groupName)) {
			final JcrPackageManager packageManager = packaging
					.getPackageManager(resourceResolver.adaptTo(Session.class));
			final PackageId packageId = new PackageId(groupName, packageName, version);
			try (JcrPackage jcrPackage = packageManager.open(packageId)) {
				if(null != jcrPackage) {
					packageManager.assemble(jcrPackage, null);
					return jcrPackage.getNode().getPath();
				}
			} catch (RepositoryException | PackageException | IOException e) {
				LOGGER.error("Could not build package {}", e.getMessage());
				return StringUtils.EMPTY;
			}
		}
		return StringUtils.EMPTY;
	}

	@Override
	public String installPackage(ResourceResolver resourceResolver, String groupName, String packageName,
			String version, ImportMode importMode, AccessControlHandling aclHandling) {
		if (StringUtils.isNotEmpty(packageName) && StringUtils.isNotEmpty(groupName)) {
			final JcrPackageManager packageManager = packaging
					.getPackageManager(resourceResolver.adaptTo(Session.class));
			final PackageId packageId = new PackageId(groupName, packageName, version);
			try (JcrPackage jcrPackage = packageManager.open(packageId)) {
				if(null != jcrPackage) {
					final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
					jcrPackage.install(opts);
					return jcrPackage.getNode().getPath();
				}
			} catch (RepositoryException | PackageException | IOException e) {
				LOGGER.error("Could not install package {}", e.getMessage());
				return StringUtils.EMPTY;
			}
		}
		return StringUtils.EMPTY;
	}

	@Override
	public String uploadAndInstallPack(ResourceResolver resourceResolver, @Nullable RequestParameter inputPackage) {
		if (null != inputPackage) {
			try (InputStream inputPack = inputPackage.getInputStream()) {
				if (null != inputPack) {
					final JcrPackageManager packageManager = packaging
							.getPackageManager(resourceResolver.adaptTo(Session.class));
					try (JcrPackage jcrPackage = packageManager.upload(inputPack, true)) {
						installPackage(jcrPackage, ImportMode.REPLACE, AccessControlHandling.IGNORE);
						return jcrPackage.getNode().getPath();
					} catch (RepositoryException e) {
						LOGGER.error("Could not Upload and Install package due to Repository Exception {}", e.getMessage());
						e.printStackTrace();
					}
				}
			} catch (IOException e) {
				LOGGER.error("Could not Upload and Install package due to IO Exception {}", e.getMessage());
				return StringUtils.EMPTY;
			}
		}
		return StringUtils.EMPTY;
	}

	public String installPackage(JcrPackage jcrPackage, final ImportMode importMode,
			final AccessControlHandling aclHandling) {
		try {
			final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
			jcrPackage.install(opts);
			return jcrPackage.getNode().getPath();
		} catch (RepositoryException | PackageException | IOException e) {
			LOGGER.error("Could not install built package {}", e.getMessage());
			return StringUtils.EMPTY;
		}
	}

	@Override
	public String deletePackage(ResourceResolver resourceResolver, String groupName, String packageName,
			String version) {
		if (StringUtils.isNotEmpty(packageName) && StringUtils.isNotEmpty(groupName)) {
			final JcrPackageManager packageManager = packaging
					.getPackageManager(resourceResolver.adaptTo(Session.class));
			final PackageId packageId = new PackageId(groupName, packageName, version);
			try (JcrPackage jcrPackage = packageManager.open(packageId)) {
				if(null != jcrPackage) {
					String path = jcrPackage.getNode().getPath();
					packageManager.remove(jcrPackage);
					return path;
				}				
			} catch (RepositoryException e) {
				LOGGER.error("Could not delete package {}", e.getMessage());
				return StringUtils.EMPTY;
			}
		}
		return StringUtils.EMPTY;
	}

}

Add the following VltUtils as shown below:

package com.aem.operations.core.utils;

import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;

/**
 * Utility class for creating vlt filters and import/export options
 */
public class VltUtils {

    public static ImportOptions getImportOptions(AccessControlHandling aclHandling, ImportMode importMode) {
        ImportOptions opts = new ImportOptions();
        if (aclHandling != null) {
            opts.setAccessControlHandling(aclHandling);
        } else {
            // default to overwrite
            opts.setAccessControlHandling(AccessControlHandling.OVERWRITE);
        }
        if (importMode != null) {
            opts.setImportMode(importMode);
        } else {
            // default to update
            opts.setImportMode(ImportMode.UPDATE);
        }

        return opts;
    }
}

Add the following HTL to render the page:

<!DOCTYPE html>
<head>
   <link rel="shortcut icon" href="/libs/granite/core/content/login/favicon.ico">
</head>
<body class="coral--light foundation-layout-util-maximized-alt">
   <sly data-sly-call="${clientLib.all @ categories=['coralui3','granite.ui.coral.foundation','granite.ui.shell','cq.authoring.dialog', 'packagehandelr.base']}"
      data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html"/>
   <coral-shell-header aria-hidden="false" aria-label="Header Bar"
      class="coral--dark granite-shell-header coral3-Shell-header"
      role="region">
      <coral-shell-header-home aria-level="2" class="globalnav-toggle" data-globalnav-toggle-href="/" role="heading">
         <a class="coral3-Shell-homeAnchor" href="/"
            icon="adobeExperienceManagerColor" is="coral-shell-homeanchor"
            style="display: inline-block; padding-right: 0;">
            <coral-icon
               aria-label="adobe experience manager color"
               class="coral3-Icon coral3-Shell-homeAnchor-icon coral3-Icon--adobeExperienceManagerColor coral3-Icon--sizeM"
               icon="adobeExperienceManagerColor" role="img"
               size="M"></coral-icon>
            <coral-shell-homeanchor-label>Adobe Experience Manager
            </coral-shell-homeanchor-label>
         </a>
         <span style="line-height: 2.375rem;">/ AEM Operations / ${properties.jcr:title} </span>
      </coral-shell-header-home>
   </coral-shell-header>
   <div class="foundation-layout-panel">
      <div class="foundation-layout-panel-header">
         <betty-titlebar>
            <betty-titlebar-title>
               <span aria-level="1" class="granite-title" role="heading">${properties.jcr:title}</span>
            </betty-titlebar-title>
            <betty-titlebar-primary></betty-titlebar-primary>
            <betty-titlebar-secondary>
            </betty-titlebar-secondary>
         </betty-titlebar>
      </div>
      <div class="content-container-inner coral-Well" style="width:60%; margin:0 auto;">
         <sly data-sly-use.packprops="com.aem.operations.core.models.PackageHandlerModel">
            <sly data-sly-resource="${packprops.formResource}"/>
         </sly>
         <coral-actionbar-item class="coral3-ActionBar-item">
            <button class="coral3-Button coral3-Button--primary package-initiator" icon="checkCircle" iconsize="S"
               is="coral-button"
               size="M" variant="primary">
               <coral-icon alt="" class="coral3-Icon coral3-Icon--sizeS coral3-Icon--checkCircle" icon="engagement"
                  size="S"></coral-icon>
               <coral-button-label>
                  Start the Process
               </coral-button-label>
            </button>
         </coral-actionbar-item>
          <!-- The Modal -->
          <div id="popup-modal" class="modal">
              <!-- Modal content -->
              <div class="modal-content">
                  <span class="close">&times;</span>
                  <span class="loading">Loading</span>
                  <span class="loader"></span>
              </div>
          </div>
      </div>
   </div>
</body>

Add the following JS code to process the ajax request as shown below:

var Coral = window.Coral || {},
    Granite = window.Granite || {};

(function (window, document, $, Coral) {
    "use strict";
    var packageOperation = "UPLOAD";
    $(document).on("foundation-contentloaded", function (e) {

        var SITE_PATH = "/conf/aemoperations/settings/tools/packagehandler-initiator.html",
            ui = $(window).adaptTo("foundation-ui");

        if (window.location.href.indexOf(SITE_PATH) < 0) {
            return;
        }

        if (packageOperation === "UPLOAD") {
            $("input[name='./packageName']").attr("disabled", true);
            $("input[name='./packageGroup']").attr("disabled", true);
            $("input[name='./packageVersion']").attr("disabled", true);
        }

        $(document).off("change", ".coral-RadioGroup").on("change", ".coral-RadioGroup", function (event) {
            packageOperation = event.target.value;
			if (packageOperation === "UPLOAD" || packageOperation === "UPLOAD_INSTALL") {
				$("input[name='./packageName']").attr("disabled", true);
				$("input[name='./packageGroup']").attr("disabled", true);
				$("input[name='./packageVersion']").attr("disabled", true);
				$("coral-fileupload[name='./inputPackage']").attr("disabled", false);
			} else if(packageOperation === "BUILD" || packageOperation === "INSTALL" || packageOperation === "DELETE"){
				$("input[name='./packageName']").attr("disabled", false);
				$("input[name='./packageGroup']").attr("disabled", false);
				$("input[name='./packageVersion']").attr("disabled", false);
				$("coral-fileupload[name='./inputPackage']").attr("disabled", true);
			}
        });

        $(document).off("change", ".close").on("change", ".close", function (event) {
            $(".modal").hide();
        });

        function getValueByName(fieldName, isMandatory) {
            var fieldValue = ($("input[name='" + fieldName + "']").val()).trim();
            if (!isMandatory) {
                return fieldValue;
            }
            if (!fieldValue || fieldValue.length === 0) {
                //for input fields
                $("input[name='" + fieldName + "']").attr('aria-invalid', 'true');
                $("input[name='" + fieldName + "']").attr('invalid', 'invalid');

                //for select fields
                $("coral-select[name='" + fieldName + "']").attr('aria-invalid', 'true');
                $("coral-select[name='" + fieldName + "']").attr('invalid', 'invalid');

                return;
            } else {
                return fieldValue;
            }
        }

        function getFileByName(fieldName) {
            var fieldInput = $("input[name='" + fieldName + "']");
            if(null != fieldInput && null != fieldInput[0]){
                return fieldInput[0].files[0];
            }
        }

        $(document).off("click", ".package-initiator").on("click", ".package-initiator", function (event) {
            event.preventDefault();

            var packageName = getValueByName('./packageName', false),
                packageGroup = getValueByName('./packageGroup', false),
                packageVersion = getValueByName('./packageVersion', false),
                inputPackage = getFileByName('./inputPackage');

            if (!packageOperation) {
                return;
            }

            var formData = new FormData();
            if(null != inputPackage){
                formData.append("file", inputPackage, inputPackage.name);
            }
            formData.append("packageName", packageName);
            formData.append("packageGroup", packageGroup);
            formData.append("packageVersion", packageVersion);
            formData.append("packageOperation", packageOperation);

			$(".loading").html("PLEASE WAIT "+packageOperation+"ING PACKAGE");
			$(".modal").show();

            $.ajax({
                url: "/bin/triggerPackageHandler",
                method: "POST",
                async: true,
                cache: false,
                contentType: false,
                processData: false,
                data: formData
            }).done(function (data) {
                if (data && data.message){
                    ui.notify("Success", data.message, "success");
                    var dialog = new Coral.Dialog();
                    dialog.id = 'dialogSuccess';
                    dialog.header.innerHTML = 'Success';
                    dialog.content.innerHTML = data.message;
                    dialog.footer.innerHTML = '<button class="ok-button" is="coral-button" variant="primary" icon="check" coral-close>OK</button>';
                    dialog.variant = 'success';
                    dialog.closable = "on";
                    dialog.show();
					$(".modal").hide();
                }else{
                    ui.notify("Error", "Unable to process package operation", "error");
                }
            }).fail(function (data) {
				$(".modal").hide();
                if (data && data.responseJSON && data.responseJSON.message){
                    ui.notify("Error", data.responseJSON.message, "error");
                }else{
                    ui.notify("Error", "Unable to process package operation", "error");
                }
            });
        });
    });
})(window, document, $, Coral);

Once the code is built and deployed you will be able to access the tool by navigating the:

Tools -> {Tool Section} -> {Tool Name}

Package Handler Tool Section

Now you can access the package handler and upload the packages to AEM as an upload or upload and install.

Package Handler Page

If you’re building any existing packages then you can build, install or delete packages. Provide package name, version, and group details to pull the package.

Provide the desired package name and Description and you can see the package path will be prompted and you will be able to download the built package by clicking on the link:

Package Handler Result
test package is uploaded

Working Code:
You can also access the working code on my GitHub repository link: https://github.com/kiransg89/aemoperations

Note: You can access this package over domain on AEM / AEMaaCS and over any environments and architects can handle the permissions by adding appropriate rep:policy on cq generator node or conf page