Delete Unreferenced Assets AEM

Problem Statement:

Delete all the assets which don’t have references to improve AEM performance in turn Indexes and search/query performance.

Introduction:

How do assets get published?

  1. The author uploads the images and publishes the assets
  2. Create a launcher and workflow which processes assets metadata and publishes the pages
  3. Whenever we publish any pages and if the page has references to assets, then during publishing, it asks to replicate the references as well.

What happens when the page is unpublished?

  1. When the page is deactivated, assets referenced to the page will not be deactivated because this asset might have reference to the other pages hence out of the box assets won’t be deactivated.
  2. If we perform cleanup, deactivate and delete old pages, we might not be cleaning up assets related to this page.

What advantages of cleaning up of old assets?

  1. Drastically reduces repository size
  2. Improves DAM Asset search
  3. Improves indexing

Generate Published Asset Report by visiting:

Go to Tools -> Assets -> Reports as shown below:

Asset Report Tool Section

Click on create and click on Publish report

Asset Publish Report

Provide folder path and start date and end date

Asset Report Configure Page

Select the columns as per the requirement

Asset Report Custom Column Page

Finally, the report will be ready with all the assets lists as shown below

Asset Report

Download the report to see the final list of images

DAM Report result

To generate the report let’s create a new AEM tool:

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.

Broken Asset Report generates a report of all the unreferenced assets by running a Reference Search query across the repository every 30s (by default), you can update the scheduler expression based on your repository size.

The scheduler also checks for the CPU/HEAP size before triggering the reference search process and for more details on Throttled scheduler please refer to the link.

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

Please refer to my GitHub repository for working code on Broken Asset Reference:

Once the repository is built and deployed, you will be able to access the Broken Asset Reference report as shown below:

Broken Asset Reference Tool Section

You can select the report just now created by you in the drop down as shown below:

Select the Asset Report from dropdown

Provide scheduler expression as per your needs and select the result refresh interval for every 10s or as per your needs

You can see the results as shown below once the process is kicked off and it will also show the current row its processing and also CPU/Heap usage.

Reference Search Running status

For some reason, if your system CPU/Heap is throttling then from the backend it takes care of not running your scheduler or you can also manually unschedule the scheduler.

Once your system’s CPU comes back to normal you can go back and select the report and schedule again and report generation picks from the current row where it was left off.

Once the processing is complete click on the report name to download the generated report.

Reference Search Completed Status
Report Excel with Has reference Column

Cross verification:

  1. You can rerun the generated report on the MCP broken asset reference.
  2. Generate the Splunk (logs) results by running a query to get all the assets to call (/content/dam) requests on dispatcher/publisher from the past 1year or so.
  3. You can also reach out to the Analytics team, requesting image impressions (data on image usage) from the past 1 year or so.

Please provide your valuable feedback in the comments.

AEM Tool – Publish / UnPublish / Delete List of pages

Problem statement:

Can we Publish / Un Publish / Delete the list of paths mentioned in an excel sheet? Or provided has a linefeed

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.

Usually, product owners or authors would like to Publish certain pages like offer/product/article pages based on business requirements and also would like to unpublish and delete to clean up the pages which are unnecessary.

This process will be really helpful during excel sheet based migration.

The list Replication process usually validates the paths and if it exists then activates or deactivates or deletes pages or content paths. It accepts an excel sheet or list of paths as a line feed.

You can also provide a list of agents to be activated/deactivated to for example only to Brightspot or Brightcove connector.

In order to create a tool please use the Tool generator to give your tool name and description: 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 com.adobe.acs.commons.mcp.form.*;
import lombok.Getter;
import lombok.Setter;
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 javax.inject.Named;
import java.io.InputStream;

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

    public enum QueueMethod {
        USE_MCP_QUEUE, USE_PUBLISH_QUEUE, MCP_AFTER_10K
    }

    public enum ReplicationAction {
        PUBLISH, UNPUBLISH, DELETE
    }

    @Getter
    @Setter
    @Named(value = "repPathExcel")
    @FormField(name = "Replication Excel", description = "Upload Replication Excel", component = FileUploadComponent.class)
    private InputStream repPathExcel = null;

    @Getter
    @Setter
    @Named(value = "queueMethod")
    @FormField(
            name = "Queueing Method",
            description = "For small publishing tasks, standard is sufficient.  For large folder trees, MCP is recommended.",
            required = true,
            component = SelectComponent.EnumerationSelector.class,
            options = {"horizontal", "default=USE_MCP_QUEUE"})
    QueueMethod queueMethod = QueueMethod.USE_MCP_QUEUE;

    @Getter
    @Setter
    @Named(value = "agents")
    @FormField(name = "Agents",
            component = TextfieldComponent.class,
            hint = "(leave blank for default agents)",
            description = "Publish agents to use, if blank then all default agents will be used. Multiple agents can be listed using commas or regex.")
    private String agents;

    @Getter
    @Setter
    @Named(value = "reAction")
    @FormField(name = "Action",
            component = SelectComponent.EnumerationSelector.class,
            description = "Publish or Unpublish?",
            options = "default=PUBLISH")
    ReplicationAction reAction = ReplicationAction.PUBLISH;
}

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 java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import com.aem.operations.core.models.ListTreeActivationModel;
import com.aem.operations.core.utils.RetryUtils;
import org.apache.commons.lang3.StringUtils;
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.PersistenceException;
import org.apache.sling.api.resource.Resource;
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.NotNull;
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.acs.commons.data.CompositeVariant;
import com.adobe.acs.commons.data.Spreadsheet;
import com.adobe.granite.rest.Constants;
import com.day.cq.replication.AgentFilter;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.Replicator;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

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

    private static final long serialVersionUID = 1L;
    private static final Logger LOGGER = LoggerFactory.getLogger(ListActiavationServlet.class);
    public static final String RESOURCE_PATH = "/bin/triggerListActivation";
    private static final String MESSAGE = "message";
    private static final String DESTINATION_PATH = "destination";
    private static int ASYNC_LIMIT = 10000;


    private List<String> agentList = new ArrayList<>();
    private Spreadsheet spreadsheet;
    AtomicInteger replicationCount = new AtomicInteger();

    @Reference
    Replicator replicator;

    @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();

        String queueMethod = request.getParameter("queueMethod");
        String agents = request.getParameter("agents");
        String reAction = request.getParameter("reAction");
        String pathsList = request.getParameter("pathsList");

        @Nullable
        RequestParameter inputPackStream = request.getRequestParameter("file");
        if(inputPackStream != null) {
            spreadsheet = new Spreadsheet(inputPackStream, DESTINATION_PATH).buildSpreadsheet();
        }
        if (null != spreadsheet) {
            AgentFilter replicationAgentFilter = prepareAgents(agents);
            if (StringUtils.equalsIgnoreCase(ListTreeActivationModel.ReplicationAction.PUBLISH.toString(), reAction)) {
                activateTreeStructure(request.getResourceResolver(), queueMethod, reAction, replicationAgentFilter);
                jsonResponse.addProperty(MESSAGE, "Succcessfully Activated all the paths provided in Excel sheet");
            } else if (StringUtils.equalsIgnoreCase(ListTreeActivationModel.ReplicationAction.UNPUBLISH.toString(), reAction)) {
                deactivateTreeStructure(request.getResourceResolver(), reAction, replicationAgentFilter);
                jsonResponse.addProperty(MESSAGE, "Succcessfully Deactivated all the paths provided in Excel sheet");
            } else {
                deleteTree(request.getResourceResolver());
                jsonResponse.addProperty(MESSAGE, "Succcessfully Deleted all the paths provided in Excel sheet");
            }
        } else if(StringUtils.isNotEmpty(pathsList)){
            AgentFilter replicationAgentFilter = prepareAgents(agents);
            Set<String> pages = new HashSet<>(Arrays.asList(StringUtils.split(pathsList, '\n'))).stream().map(String::trim).collect(Collectors.toSet());
            if (StringUtils.equalsIgnoreCase(ListTreeActivationModel.ReplicationAction.PUBLISH.toString(), reAction)) {
                activateTreeStructure(request.getResourceResolver(), pages, queueMethod, reAction, replicationAgentFilter);
                jsonResponse.addProperty(MESSAGE, "Succcessfully Activated all the paths provided in Excel sheet");
            } else if (StringUtils.equalsIgnoreCase(ListTreeActivationModel.ReplicationAction.UNPUBLISH.toString(), reAction)) {
                deactivateTreeStructure(request.getResourceResolver(), pages, reAction, replicationAgentFilter);
                jsonResponse.addProperty(MESSAGE, "Succcessfully Deactivated all the paths provided in Line Feed");
            } else {
                deleteTree(request.getResourceResolver(), pages);
                jsonResponse.addProperty(MESSAGE, "Succcessfully Deleted all the paths provided in Line Feed");
            }
        } else {
            jsonResponse.addProperty(MESSAGE, "Unable to process Activation request because of invalid Inputs");
        }
        try (PrintWriter out = response.getWriter()) {
            out.print(new Gson().toJson(jsonResponse));
        }
    }

    private AgentFilter prepareAgents(String agents) {
        AgentFilter replicationAgentFilter;
        if (StringUtils.isEmpty(agents)) {
            replicationAgentFilter = AgentFilter.DEFAULT;
        } else {
            agentList = Arrays.asList(agents.toLowerCase(Locale.ENGLISH).split(","));
            replicationAgentFilter = agent -> agentList.stream()
                    .anyMatch(p -> p.matches(agent.getId().toLowerCase(Locale.ENGLISH)));
        }
        return replicationAgentFilter;
    }

    private void activateTreeStructure(@NotNull ResourceResolver resourceResolver, String queueMethod, String reAction, AgentFilter replicationAgentFilter) {
        spreadsheet.getDataRowsAsCompositeVariants()
                .forEach(row -> performReplication(resourceResolver, getString(row, DESTINATION_PATH), queueMethod, reAction, replicationAgentFilter));
    }

    private void deactivateTreeStructure(@NotNull ResourceResolver resourceResolver, String reAction, AgentFilter replicationAgentFilter) {
        spreadsheet.getDataRowsAsCompositeVariants()
                .forEach(row -> performAsynchronousReplication(resourceResolver, getString(row, DESTINATION_PATH), reAction, replicationAgentFilter));
    }

    private void activateTreeStructure(@NotNull ResourceResolver resourceResolver, Set<String> pages, String queueMethod, String reAction, AgentFilter replicationAgentFilter) {
        pages.stream().forEach(path -> performReplication(resourceResolver, path, queueMethod, reAction, replicationAgentFilter));
    }

    private void deactivateTreeStructure(@NotNull ResourceResolver resourceResolver, Set<String> pages, String reAction, AgentFilter replicationAgentFilter) {
        pages.stream().forEach(path -> performAsynchronousReplication(resourceResolver, path, reAction, replicationAgentFilter));
    }

    private void performReplication(@NotNull ResourceResolver resourceResolver, String path, String queueMethod, String reAction, AgentFilter replicationAgentFilter) {
        int counter = replicationCount.incrementAndGet();
        if (StringUtils.equalsIgnoreCase(ListTreeActivationModel.QueueMethod.USE_MCP_QUEUE.toString(), queueMethod)
                || (StringUtils.equalsIgnoreCase(ListTreeActivationModel.QueueMethod.MCP_AFTER_10K.toString(),
                queueMethod) && counter >= ASYNC_LIMIT)) {
            performSynchronousReplication(resourceResolver, path, reAction, replicationAgentFilter);
        } else {
            performAsynchronousReplication(resourceResolver, path, reAction, replicationAgentFilter);
        }
    }

    private void performSynchronousReplication(@NotNull ResourceResolver resourceResolver, String path, String reAction, AgentFilter replicationAgentFilter) {
        ReplicationOptions options = buildOptions(replicationAgentFilter);
        options.setSynchronous(true);
        scheduleReplication(resourceResolver, options, path, reAction);
    }

    private void performAsynchronousReplication(@NotNull ResourceResolver resourceResolver, String path, String reAction, AgentFilter replicationAgentFilter) {
        ReplicationOptions options = buildOptions(replicationAgentFilter);
        options.setSynchronous(false);
        scheduleReplication(resourceResolver, options, path, reAction);
    }

    private void scheduleReplication(@NotNull ResourceResolver resourceResolver, ReplicationOptions options,
                                     String path, String reAction) {
        Session session = resourceResolver.adaptTo(Session.class);
        boolean isPublish = StringUtils
                .equalsIgnoreCase(ListTreeActivationModel.ReplicationAction.PUBLISH.toString(), reAction);
        try {
            replicator.replicate(session,
                    isPublish ? ReplicationActionType.ACTIVATE : ReplicationActionType.DEACTIVATE, path, options);
        } catch (ReplicationException e) {
            LOGGER.error("Error occured during replication {}", e.getMessage());
        }
    }

    @SuppressWarnings("rawtypes")
    private void deleteTree(ResourceResolver resourceResolver) {
        try {
            RetryUtils.withRetry(30, 500, () -> {
                Iterator<Map<String, CompositeVariant>> spredIterator = spreadsheet.getDataRowsAsCompositeVariants()
                        .iterator();
                while (spredIterator.hasNext()) {
                    Map<String, CompositeVariant> row = spredIterator.next();
                    deleteContent(resourceResolver, getString(row, DESTINATION_PATH));
                }
                commitChanges(resourceResolver);
            });
        } catch (Exception e) {
            LOGGER.error("Error occured during Retrying {}", e.getMessage());
        }
    }

    private void deleteTree(ResourceResolver resourceResolver, Set<String> pages) {
        try {
            RetryUtils.withRetry(30, 500, () -> {
                pages.forEach(path -> deleteContent(resourceResolver, path));
                commitChanges(resourceResolver);
            });
        } catch (Exception e) {
            LOGGER.error("Error occured during Retrying {}", e.getMessage());
        }
    }

    private void deleteContent(ResourceResolver resourceResolver, String destinationPath) {
        try {
            @Nullable
            Resource destinationResource = resourceResolver.getResource(destinationPath);
            if (null != destinationResource) {
                resourceResolver.delete(destinationResource);
            }
        } catch (PersistenceException e) {
            LOGGER.error("unable to delete content {}", e.getMessage());
        }
    }

    private void commitChanges(ResourceResolver resourceResolver) throws PersistenceException {
        if (resourceResolver.hasChanges()) {
            resourceResolver.commit();
            resourceResolver.refresh();
        }
    }

    private ReplicationOptions buildOptions(AgentFilter replicationAgentFilter) {
        ReplicationOptions options = new ReplicationOptions();
        options.setFilter(replicationAgentFilter);
        return options;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private String getString(Map<String, CompositeVariant> row, String path) {
        CompositeVariant v = row.get(path.toLowerCase(Locale.ENGLISH));
        if (v != null) {
            return (String) v.getValueAs(String.class);
        } else {
            return null;
        }
    }
}

Add the following Retry Utils as shown below:

package com.aem.operations.core.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RetryUtils {
    private static final Logger log = LoggerFactory.getLogger(RetryUtils.class);

    public static interface CallToRetry {
        void process() throws Exception;
    }

    public static boolean withRetry(int maxTimes, long intervalWait, CallToRetry call) throws Exception {
        if (maxTimes <= 0) {
            throw new IllegalArgumentException("Must run at least one time");
        }
        if (intervalWait <= 0) {
            throw new IllegalArgumentException("Initial wait must be at least 1");
        }
        Exception thrown = null;
        for (int i = 0; i < maxTimes; i++) {
            try {
                call.process();
                return true;
            } catch (Exception e) {
                thrown = e;
                log.info("Encountered failure on {} due to {}, attempt retry {} of {}", call.getClass().getName() , e.getMessage(), (i + 1), maxTimes, e);
            }
            try {
                Thread.sleep(intervalWait);
            } catch (InterruptedException wakeAndAbort) {
                break;
            }
        }
        throw thrown;
    }
}

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', 'listreplication.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;">
         <div class="acsCommons-System-Notifications-Form-row">
		   <label class="coral-Form-fieldlabel">List of Paths</label>
		   <label> Note: Add the list of paths has a Line feed (No need for comma separation)</label>
		   <textarea
		      class="coral-Textfield coral-Textfield--multiline acsCommons-System-Notifications-Page-input--textarea"
		      rows="6"
		      name="./pathsList"></textarea>
		 </div>
		 <label> Note: Create a Excel Sheet with column called <b>destination</b> and the list of values and upload</label>
         <sly data-sly-use.packprops="com.aem.operations.core.models.ListTreeActivationModel">
            <sly data-sly-resource="${packprops.formResource}"/>
         </sly>
         <coral-actionbar-item class="coral3-ActionBar-item">
            <button class="coral3-Button coral3-Button--primary activation-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="publish"
                  size="S"></coral-icon>
               <coral-button-label>
                  Start the Replication
               </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";
    $(document).on("foundation-contentloaded", function (e) {

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

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

        $(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 getValueByNameArea(fieldName, isMandatory) {
            var fieldValue = ($("textarea[name='" + fieldName + "']").val()).trim();
            if (!isMandatory) {
                return fieldValue;
            }
            if (!fieldValue || fieldValue.length === 0) {
                //for input fields
                $("textarea[name='" + fieldName + "']").attr('aria-invalid', 'true');
                $("textarea[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", ".activation-initiator").on("click", ".activation-initiator", function (event) {
            event.preventDefault();

            var queueMethod = getValueByName('./queueMethod', false),
                agents = getValueByName('./agents', false),
                reAction = getValueByName('./reAction', false),
                pathsList = getValueByNameArea('./pathsList', true),
                repPathExcel = getFileByName('./repPathExcel');

            var formData = new FormData();
            if(null != repPathExcel){
                formData.append("file", repPathExcel, repPathExcel.name);
            }
            formData.append("queueMethod", queueMethod);
            formData.append("agents", agents);
            formData.append("reAction", reAction);
            formData.append("pathsList", pathsList);

			$(".loading").html("Replication in progress");
            $(".modal").show();

            $.ajax({
                url: "/bin/triggerListActivation",
                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 List Activation", "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 List Activation", "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}

List Process Section

Now you can input the page paths as a line feed (no need to separate the paths using commas or any characters)

List Process Page

Upload the excel sheet or paths and select the queue method it’s recommended to use MCP Queue if you are activating more than 10K pages then it’s recommended to select MCP After 10K.

Select the Action to be performed.

List Replication Result
Page activation result

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

AEM Tool – Create a package using a list of paths

Problem statement:

How to create a package using a list of paths?

Avoid adding individual paths manually in the CRX Package manager

Option to package current page or pull children

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 Creator is an extension of the ACS Commons Package handler, but it’s UI friendly and easy to access. You can input the paths to be packaged as a line feed and no need to separate them by a comma. Once the package is ready it will be built and prompted with a download option.

In order to create a 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 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.FormField;
import com.adobe.acs.commons.mcp.form.GeneratedDialog;
import com.adobe.acs.commons.mcp.form.SelectComponent;
import lombok.Getter;
import lombok.Setter;

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

    public enum GetChildren {
        NO, YES
    }

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

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

    @Getter
    @Setter
    @Named(value = "getChildren")
    @FormField(
            name = "Get Children",
            description = "child pages will be pulled",
            required = true,
            component = SelectComponent.EnumerationSelector.class,
            options = {"horizontal", "default=NO"})
    GetChildren getChildren = GetChildren.NO;
}

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

package com.aem.operations.core.servlets;

import com.adobe.acs.commons.packaging.PackageHelper;
import com.adobe.granite.rest.Constants;
import com.aem.operations.core.models.PackageCreatorModel;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.packaging.*;
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.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 javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.stream.Collectors;

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

    private static final long serialVersionUID = 1L;
    private static final Logger LOGGER = LoggerFactory.getLogger(PackageCreatorServlet.class);
    public static final String RESOURCE_PATH = "/bin/triggerPackageCreator";
    private static final String MESSAGE = "message";
    private static final String CONTENT_DAM_SLASH = "/content/dam/";

    private static final String DEFUALT_GROUP_NAME = "my_packages";
    private static final String DEFUALT_VERSION = "1.0";
    private static final String QUERY_PACKAGE_THUMBNAIL_RESOURCE_PATH = "/apps/acs-commons/components/utilities/packager/query-packager/definition/package-thumbnail.png";

    @Reference
    private PackageHelper packageHelper;

    @Reference
    private Packaging packaging;

    @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 packageDescription = request.getParameter("packageGroup");
        String pathsList = request.getParameter("pathsList");
        String getChildren = request.getParameter("getChildren");

        String packagePath = StringUtils.EMPTY;
        if(StringUtils.isNotEmpty(pathsList)) {
            packagePath = createPackage(resourceResolver, packageName, packageDescription, pathsList, getChildren);
        }

        if(StringUtils.isNotEmpty(packagePath)) {
            jsonResponse.addProperty(MESSAGE, "Package creation is Successful and path is <a href=\"" + packagePath + "\">" + packagePath + "</a>");
        } else {
            LOGGER.error("Unable to process package creation");
        }
        try (PrintWriter out = response.getWriter()) {
            out.print(new Gson().toJson(jsonResponse));
        }
    }

    private String createPackage(ResourceResolver resourceResolver, String packageName, String packageDescription,
                                 String pathsList, String getChildren) throws IOException {
        Set<String> pages = new HashSet<>(Arrays.asList(StringUtils.split(pathsList, '\n'))).stream().map(String::trim).collect(Collectors.toSet());
        Set<Resource> packageResources = pages.stream().map(path -> getResource(resourceResolver, path, getChildren)).collect(Collectors.toSet());
        if(null != packageResources && !packageResources.isEmpty()) {
            Map<String, String> packageDefinitionProperties = new HashMap<>();
            // ACL Handling
            packageDefinitionProperties.put(JcrPackageDefinition.PN_AC_HANDLING,
                    AccessControlHandling.OVERWRITE.toString());
            // Package Description
            packageDefinitionProperties.put(JcrPackageDefinition.PN_DESCRIPTION, packageDescription);
            try(JcrPackage jcrPackage = packageHelper.createPackage(packageResources,
                    resourceResolver.adaptTo(Session.class), DEFUALT_GROUP_NAME, packageName, DEFUALT_VERSION,
                    PackageHelper.ConflictResolution.Replace, packageDefinitionProperties)){
                // Add thumbnail to the package definition
                packageHelper.addThumbnail(jcrPackage, resourceResolver.getResource(QUERY_PACKAGE_THUMBNAIL_RESOURCE_PATH));
                final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
                packageManager.assemble(jcrPackage, null);
                return jcrPackage.getNode().getPath();
            } catch (RepositoryException | PackageException e) {
                LOGGER.error("Exception occurred during package creation {}", e.getMessage());
            }
        }
        return StringUtils.EMPTY;
    }

    private @Nullable Resource getResource(ResourceResolver resourceResolver, String path, String getChildren) {
        if(StringUtils.equalsIgnoreCase(PackageCreatorModel.GetChildren.NO.toString(), getChildren) && !StringUtils.contains(path, CONTENT_DAM_SLASH)) {
            return resourceResolver.resolve(path + "/jcr:content");
        }
        return resourceResolver.resolve(path);
    }
}

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', 'packagecreator.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;">
         <div class="acsCommons-System-Notifications-Form-row">
		   <label class="coral-Form-fieldlabel">List of Paths</label>
		   <label> Note: Add the list of paths has a Line feed (No need for comma separation)</label>
		   <textarea
		      class="coral-Textfield coral-Textfield--multiline acsCommons-System-Notifications-Page-input--textarea"
		      rows="6"
		      name="./pathsList"></textarea>
		 </div>
         <sly data-sly-use.packprops="com.aem.operations.core.models.PackageCreatorModel">
            <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="actions"
                  size="S"></coral-icon>
               <coral-button-label>
                  Create Package
               </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";
    $(document).on("foundation-contentloaded", function (e) {

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

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

        $(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 getValueByNameArea(fieldName, isMandatory) {
            var fieldValue = ($("textarea[name='" + fieldName + "']").val()).trim();
            if (!isMandatory) {
                return fieldValue;
            }
            if (!fieldValue || fieldValue.length === 0) {
                //for input fields
                $("textarea[name='" + fieldName + "']").attr('aria-invalid', 'true');
                $("textarea[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;
            }
        }

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

            var packageName = getValueByName('./packageName', true),
                packageDescription = getValueByName('./packageDescription', true),
                pathsList = getValueByNameArea('./pathsList', true),
                getChildren = getValueByName('./getChildren', true);

            var formData = new FormData();
            formData.append("packageName", packageName);
            formData.append("packageDescription", packageDescription);
            formData.append("pathsList", pathsList);
            formData.append("getChildren", getChildren);

			$(".loading").html("PLEASE WAIT CREATINGING PACKAGE");
            $(".modal").show();

            $.ajax({
                url: "/bin/triggerPackageCreator",
                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 create package ", "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 create package", "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 Creator

Now you can input the page paths as a line feed (no need to separate the paths using commas or any characters)

Creator Page

Provide the list paths to be backed up, desired package name, and description and select the dropdown to pull children or not. Once the package is built you can see the package path will be prompted and you will be able to download the package by clicking on the built package link:

Package Creator Results
The package is built with the exact page location

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

AEM MCP based Bulk Replication

Problem statement:

How to bulk replicate (Publish/Unpublish) pages in AEM using MCP?

Requirement:

Use bulk replication other than the OOTB tree activation tool and using MCP add a different replication agent instead of the default agent.

Introduction:

Usually for bulk replication we usually use the OOTB tree activation page and select the path to start the bulk replication i.e, activation only.

Activation Tree
start path and activate

MCP provides an easier way and with better UI to run bulk activation. You can also create a separate replication agent other than default agent so that existing authoring replication or existing schedulers won’t be blocked.

You can use the MCP queue to replicate synchronously and the default queue to replicate asynchronously. For pages more than 10K its recommended to use MCP 10k Queue

Select the Tree Activation:

Go to MCP page url: http://{domain}/apps/acs-commons/content/manage-controlled-processes.html

Select Tree Activation process

Provide the path of the page and select all for “What to Publish” and MCP Queue based on your requirement.

Provide an agent if you are using a different agent for bulk replication and select action to bulk publish or unpublish the pages.

Tree Activation Options

For excel sheet based activation use:

AEM Publish / UnPublish / Delete List of pages – MCP Process

AEM Publish / UnPublish / Delete List of pages – MCP Process

Problem Statement:

Can we Publish / Un Publish / Delete the list of pages mentioned in an excel sheet?

Requirement:

Create an MCP process to Publish / Un Publish / Delete the list of pages mentioned in an excel sheet.

Introduction:

Usually, product owners or authors would like to Publish certain pages like offer/product/article pages based on business requirements and also would like to unpublish and delete to clean up the pages which are unnecessary.

This process will be really helpful during excel sheet based migration.

MCP (Manage Controlled Processes) is both a dashboard for performing complex tasks and a rich API for defining these tasks as process definitions. In addition to kicking off new processes, users can also monitor running tasks, retrieve information about completed tasks, halt work, and so on.

Add the following maven dependency to your pom to extend MCP

<dependency>
    <groupId>com.adobe.acs</groupId>
    <artifactId>acs-aem-commons-bundle</artifactId>
    <version>5.0.4</version>
    <scope>provided</scope>
</dependency>

Create a new process called ListTreeReplicationFactory service using MCP process definition

package com.mysite.mcp.process;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;
import com.day.cq.replication.Replicator;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class ListTreeReplicationFactory extends ProcessDefinitionFactory<ListTreeReplication> {

    @Reference
    Replicator replicator;

    @Override
    public String getName() {
        return "List Tree Activation";
    }

    @Override
    protected ListTreeReplication createProcessDefinitionInstance() {
        return new ListTreeReplication(replicator);
    }
}

Create the implementation class called ListTreeReplication and add the following logic to read the excel sheet

package com.mysite.mcp.process;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import com.adobe.acs.commons.data.CompositeVariant;
import com.adobe.acs.commons.data.Spreadsheet;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.CheckboxComponent;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.form.SelectComponent;
import com.adobe.acs.commons.mcp.form.TextfieldComponent;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.day.cq.replication.AgentFilter;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.Replicator;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.WCMException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ListTreeReplication extends ProcessDefinition {

    private static final Logger LOGGER = LoggerFactory.getLogger(ListTreeReplication.class);
	private static final String DESTINATION_PATH = "destination";
	private final GenericReport report = new GenericReport();
	private static final String REPORT_NAME = "TreeReplication-report";
	private static final String RUNNING = "Running "; 
	private static final String EXECUTING_KEYWORD = " Tree Replication";

    protected enum QueueMethod {
        USE_PUBLISH_QUEUE, USE_MCP_QUEUE, MCP_AFTER_10K
    }

    protected enum ReplicationAction {
        PUBLISH, UNPUBLISH, DELETE
    }

    private static int ASYNC_LIMIT = 10000;

    Replicator replicatorService;

    @FormField(name = "Replication Excel", component = FileUploadComponent.class)
    private RequestParameter repPathExcel;
    private Spreadsheet spreadsheet;

    @FormField(name = "Queueing Method",
            component = SelectComponent.EnumerationSelector.class,
            description = "For small publishing tasks, standard is sufficient.  For large folder trees, MCP is recommended.",
            options = "default=USE_MCP_QUEUE")
    QueueMethod queueMethod = QueueMethod.USE_MCP_QUEUE;

    @FormField(name = "Agents",
            component = TextfieldComponent.class,
            hint = "(leave blank for default agents)",
            description = "Publish agents to use, if blank then all default agents will be used. Multiple agents can be listed using commas or regex.")
    private String agents;
    List<String> agentList = new ArrayList<>();
    AgentFilter replicationAgentFilter;

    @FormField(name = "Action",
            component = SelectComponent.EnumerationSelector.class,
            description = "Publish or Unpublish?",
            options = "default=PUBLISH")
    ReplicationAction reAction = ReplicationAction.PUBLISH;

    @FormField(name = "Dry Run",
            component = CheckboxComponent.class,
            options = "checked",
            description = "If checked, only generate a report but don't perform the work"
    )
    private boolean dryRun = true;

    public ListTreeReplication(Replicator replicator) {
        replicatorService = replicator;
    }

    @Override
    public void init() throws RepositoryException {
    	try {
            // Read spreadsheet
            spreadsheet = new Spreadsheet(repPathExcel, DESTINATION_PATH).buildSpreadsheet();
        } catch (IOException e) {
            throw new RepositoryException("Unable to process spreadsheet", e);
        }
    }

    @Override
    public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException, RepositoryException {    	    	
        report.setName(REPORT_NAME);                
        instance.getInfo().setDescription(RUNNING + reAction + EXECUTING_KEYWORD);    
        if (reAction == ReplicationAction.PUBLISH) {
            instance.defineCriticalAction("Activate tree structure", rr, this::activateTreeStructure);
            if (StringUtils.isEmpty(agents)) {
                replicationAgentFilter = AgentFilter.DEFAULT;
            } else {
                agentList = Arrays.asList(agents.toLowerCase(Locale.ENGLISH).split(","));
                replicationAgentFilter = agent -> agentList.stream().anyMatch(p -> p.matches(agent.getId().toLowerCase(Locale.ENGLISH)));
            }
        } else if (reAction == ReplicationAction.UNPUBLISH) {
            instance.defineCriticalAction("Deactivate tree structure", rr, this::deactivateTreeStructure);
        } else {
        	instance.defineCriticalAction("Delete tree structure", rr, this::deleteTreeStructure);
        }
    }

    public enum ReportColumns {
        PATH, ACTION, DESCRIPTION
    }

    List<EnumMap<ReportColumns, String>> reportData = Collections.synchronizedList(new ArrayList<>());

    private void recordAction(String path, String action, String description) {
        EnumMap<ReportColumns, String> row = new EnumMap<>(ReportColumns.class);
        row.put(ReportColumns.PATH, path);
        row.put(ReportColumns.ACTION, action);
        row.put(ReportColumns.DESCRIPTION, description);
        reportData.add(row);
    }

    @Override
    public void storeReport(ProcessInstance instance, ResourceResolver rr) throws RepositoryException, PersistenceException {    	
        report.setRows(reportData, ReportColumns.class);
        report.persist(rr, instance.getPath() + "/jcr:content/report");
    }

    private void activateTreeStructure(ActionManager t) {
    	spreadsheet.getDataRowsAsCompositeVariants().forEach(row -> performReplication(t, getString(row, DESTINATION_PATH)));    	
    }

    private void deactivateTreeStructure(ActionManager t) {
    	spreadsheet.getDataRowsAsCompositeVariants().forEach(row -> performAsynchronousReplication(t, getString(row, DESTINATION_PATH)));
    }
    
    private void deleteTreeStructure(ActionManager t) {
    	spreadsheet.getDataRowsAsCompositeVariants().forEach(row -> t.deferredWithResolver(r -> deletePage(r, getString(row, DESTINATION_PATH))));
    }

    private void deletePage(ResourceResolver resourceResolver, String destinationPath) {    	
    	try {
    		final PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
        	Page destinationPage = resourceResolver.resolve(destinationPath).adaptTo(Page.class);
			pageManager.delete(destinationPage, false, true);
			recordAction(destinationPath, "Deletion", "Synchronous delete");
		} catch (WCMException e) {
            LOGGER.error("unable to delete page {}", e.getMessage());
		}
	}
    
    AtomicInteger replicationCount = new AtomicInteger();

    private void performReplication(ActionManager t, String path) {
        int counter = replicationCount.incrementAndGet();
        if (queueMethod == QueueMethod.USE_MCP_QUEUE
                || (queueMethod == QueueMethod.MCP_AFTER_10K && counter >= ASYNC_LIMIT)) {
            performSynchronousReplication(t, path);
        } else {
            performAsynchronousReplication(t, path);
        }
    }

    private void performSynchronousReplication(ActionManager t, String path) {
        ReplicationOptions options = buildOptions();
        options.setSynchronous(true);
        scheduleReplication(t, options, path);
        recordAction(path, reAction == ReplicationAction.PUBLISH ? "Publish" : "Unpublish", "Synchronous replication");
    }

    private void performAsynchronousReplication(ActionManager t, String path) {
        ReplicationOptions options = buildOptions();
        options.setSynchronous(false);
        scheduleReplication(t, options, path);
        recordAction(path, reAction == ReplicationAction.PUBLISH ? "Publish" : "Unpublish", "Asynchronous replication");
    }

    private ReplicationOptions buildOptions() {
        ReplicationOptions options = new ReplicationOptions();
        options.setFilter(replicationAgentFilter);
        return options;
    }

    private void scheduleReplication(ActionManager t, ReplicationOptions options, String path) {
        if (!dryRun) {
            t.deferredWithResolver(rr -> {
                Session session = rr.adaptTo(Session.class);
                replicatorService.replicate(session, reAction == ReplicationAction.PUBLISH ? ReplicationActionType.ACTIVATE : ReplicationActionType.DEACTIVATE, path, options);
            });
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private String getString(Map<String, CompositeVariant> row, String path) {
        CompositeVariant v = row.get(path.toLowerCase(Locale.ENGLISH));
        if (v != null) {
            return (String) v.getValueAs(String.class);
        } else {
            return null;
        }
    }
}

Once code is deployed, please go to the following URL and click on start process as shown below:

http://{domain}/apps/acs-commons/content/manage-controlled-processes.html

You will see a new process called List Tree Activation as shown below:

Click on the process

Create an excel sheet with a column called destination and add the page paths.

List Replication sheet


Note: Please make sure do not run delete before unpublish.

Excelsheet: