Purge Old CRX Packages AEM


Problem statement:

AEM environment size is increasing because of user-generated packages


Requirement:

Can we purge all the user-generated packages to improve stability?


Introduction:

A package is a zip file holding repository content in the form of a file-system serialization (called “vault” serialization). This provides an easy-to-use-and-edit representation of files and folders.

Packages include content, both page content and project-related content, selected using filters.

A package also contains vault meta information, including the filter definitions and import configuration information. Additional content properties (that are not used for package extraction) can be included in the package, such as a description, a visual image, or an icon; these properties are for the content package consumer and for informational purposes only.

Usually, Developers or AEM content synch or even code deployment will keep on piling up in the CRX packages and it will be consuming spaces on MBs and even sometimes GBs.

If we move more packages, then loading crx/packmgr would more time.

Hence you can create a scheduler that runs on off-hours which cleans up the packages and which will get back the space and avoids extra maintenance tasks.

The below scheduler will remove all the packages for my_package group we can add business logic to handle for other groups

package com.mysite.core.schedulers;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "Old Packages Purge Schedular", description = "Remove old packages from different paths")
public @interface PurgeOldPackagesSchedulerConfig {

    String DEFAULT_SCHEDULER_EXPRESSION = "0 0 16 ? * SUN *"; // every Sunday 4 PM
    boolean DEFAULT_SCHEDULER_CONCURRENT = false;

    @AttributeDefinition(name = "Enabled", description = "True, if scheduler service is enabled", type = AttributeType.BOOLEAN)
    boolean enabled() default true;

    @AttributeDefinition(name = "Cron expression defining when this Scheduled Service will run", description = "[every minute = 0 * * * * ?], [12:00am daily = 0 0 0 ? * *]", type = AttributeType.STRING)
    String schedulerExpression() default DEFAULT_SCHEDULER_EXPRESSION;

    @AttributeDefinition(name = "package paths", description = "package folder paths", type = AttributeType.STRING)
    String[] packagesPaths() default {"my_packages"};
}
package com.mysite.core.schedulers;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
import org.apache.jackrabbit.vault.packaging.Packaging;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(immediate = true, service = PurgeOldPackagesScheduler.class, configurationPolicy = ConfigurationPolicy.REQUIRE)
@Designate(ocd = PurgeOldPackagesSchedulerConfig.class)
public class PurgeOldPackagesScheduler implements Runnable {

    private static final Logger log = LoggerFactory.getLogger(PurgeOldPackagesScheduler.class);
    /**
     * Id of the scheduler based on its name
     */
    private String schedulerJobName;

    Session session;
    private List<String> packagesPathsList;

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    @Reference
    private Packaging packaging;

    @Reference
    private Scheduler scheduler;

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Activate
    @Modified
    protected void activate(PurgeOldPackagesSchedulerConfig purgeOldPackagesSchedulerConfig) {
        /**
         * Creating the scheduler id
         */

        this.schedulerJobName = this.getClass().getSimpleName();
        addScheduler(purgeOldPackagesSchedulerConfig);
        packagesPathsList = Arrays.asList(purgeOldPackagesSchedulerConfig.packagesPaths());
    }

    /**
     * @see Runnable#run().
     */
    @Override
    public final void run() {
        log.debug("PurgeOldPackagesScheduler Job started");
        purgeOldPackages(packagesPathsList);
        log.debug("PurgeOldPackagesScheduler Job completed");
    }

    void purgeOldPackages(List<String> packagesPathsList) {
        try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(Collections
                .singletonMap(ResourceResolverFactory.SUBSERVICE, ServiceUserConstants.ADMINISTRATIVE_SERVICE_USER))) {
            session = resourceResolver.adaptTo(Session.class);
            JcrPackageManager jcrPackageManager = packaging.getPackageManager(session);
            packagesPathsList.forEach(group -> {
                removePackages(jcrPackageManager, 0, group);
            });
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if (session != null) {
                session.logout();
            }
        }
    }

    @Deactivate
    protected void deactivate() {

        /**
         * Removing the scheduler
         */
        removeScheduler();
    }

    /**
     * This method adds the scheduler
     *
     * @param purgeOldPackagesSchedulerConfig
     */
    private void addScheduler(PurgeOldPackagesSchedulerConfig purgeOldPackagesSchedulerConfig) {

        /**
         * Check if the scheduler is enabled
         */
        if (purgeOldPackagesSchedulerConfig.enabled()) {

            /**
             * Scheduler option takes the cron expression as a parameter and run accordingly
             */
            ScheduleOptions scheduleOptions = scheduler.EXPR(purgeOldPackagesSchedulerConfig.schedulerExpression());

            /**
             * Adding some parameters
             */
            scheduleOptions.name(schedulerJobName);
            scheduleOptions.canRunConcurrently(false);

            /**
             * Scheduling the job
             */
            scheduler.schedule(this, scheduleOptions);

            log.info("{} Scheduler added", schedulerJobName);
        } else {
            log.info("Scheduler is disabled");
            removeScheduler();
        }

    }

    /**
     * This method removes the scheduler
     */
    private void removeScheduler() {

        log.info("Removing scheduler: {}", schedulerJobName);

        /**
         * Unscheduling/removing the scheduler
         */
        scheduler.unschedule(String.valueOf(schedulerJobName));
    }

    private void removePackages(JcrPackageManager jcrPackageManager, int counterStart, String groupName) {
        try {
            List<JcrPackage> packages = jcrPackageManager.listPackages(groupName, false);
            AtomicInteger counter = new AtomicInteger(counterStart);
            if (null != packages && !packages.isEmpty()) {
                packages.stream().sorted(Comparator.nullsLast((e1, e2) -> {
                    try {
                        return ((JcrPackage) e1).getPackage().getCreated()
                                .compareTo(((JcrPackage) e2).getPackage().getCreated());
                    } catch (RepositoryException | IOException ex) {
                        log.error(ex.getMessage());
                    }
                    return -1;
                }).reversed()).forEach(pack -> {
                    if (counter.incrementAndGet() > 3) {
                        try {
                            jcrPackageManager.remove(pack);
                        } catch (RepositoryException ex) {
                            log.error(ex.getMessage());
                        }
                    }
                });
            }
        } catch (RepositoryException e) {
            e.printStackTrace();
        }
    }
}

Bulk Add, Update and Delete properties in AEM – without using Groovy console

Problem Statement:

How to Bulk Add, Update or remove page properties in AEM? Without using the Groovy console.

Requirement:

Create a reusable process that can be used to search for the pages based on resourceType and do the CRUD operations on the results.

Introduction:

Usually, whenever we are using editable templates, we might have some initial content but for some reason, if we want to update the experience fragment path or some page properties then usually, we go for Groovy script to run bulk update.

But AMS don’t install developer tools on the PROD, we need to go to other options and for the above requirement, we can use MCP.

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 Process Definition factory – PropertyUpdateFactory

This class tells ACS Commons MCP to pick the process definition and process name getName and you need to mention the implementation class inside the createProcessDefinitionInstance method as shown below:

package com.mysite.mcp.process;

import org.osgi.service.component.annotations.Component;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class PropertyUpdateFactory extends ProcessDefinitionFactory<PropertyUpdater> {
    @Override
    public String getName() {
        return "Property Updator";
    }
    @Override
    protected PropertyUpdater createProcessDefinitionInstance() {
        return new PropertyUpdater();
    }
}

Create Process Definition implementation – PropertyUpdater

This is an implementation class where we are defining all the form fields required for the process to run

package com.mysite.mcp.process;

import java.io.IOException;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.jcr.RepositoryException;
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.ModifiableValueMap;
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.resource.ResourceUtil;
import org.apache.sling.resource.filter.ResourceFilterStream;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.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.commons.jcr.JcrConstants;

public class PropertyUpdater extends ProcessDefinition {

    private static final Logger LOGGER = LoggerFactory.getLogger(PropertyUpdater.class);
    private final GenericReport report = new GenericReport();
    private static final String REPORT_NAME = "Property-update-report";
    private static final String RUNNING = "Running ";
    private static final String EXECUTING_KEYWORD = " Property Updation";

    private static final String PROPERTY = "property";
    private static final String PROPERTY_TYPE = "type";
    private static final String PROPERTY_VALUE = "value";

    protected enum UpdateAction {
        ADD, UPDATE, DELETE
    }

    @FormField(name = "Property Update Excel", component = FileUploadComponent.class)
    private RequestParameter sourceFile;

    @FormField(name = "Path",
            component = TextfieldComponent.class,
            hint = "(Provide the Relative path to start search)",
            description = "A query will be executed starting this path")
    private String path;

    @FormField(name = "Action",
            component = SelectComponent.EnumerationSelector.class,
            description = "Add, Update or Delete?",
            options = "default=Add")
    UpdateAction reAction = UpdateAction.ADD;

    @FormField(name = "ResourceType",
            component = TextfieldComponent.class,
            hint = "(Provide the resourcetype to be searched for)",
            description = "A query will be executed based on resourcetype")
    private String pageResourceType;

    private Map<String, Object> propertyMap = new HashMap<>();

    @Override
    public void init() throws RepositoryException {
        validateInputs();
    }

    @Override
    public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException, RepositoryException {
        report.setName(REPORT_NAME);
        instance.getInfo().setDescription(RUNNING + reAction + EXECUTING_KEYWORD);
        instance.defineCriticalAction("Adding the Properties", rr, this::updateProperties);
    }

    private void updateProperties(ActionManager manager) {
        manager.deferredWithResolver(this::addProperties);
    }

    private void addProperties(ResourceResolver resourceResolver) {
        @NotNull Resource resource = resourceResolver.resolve(path);
        if(!ResourceUtil.isNonExistingResource(resource)) {
            ResourceFilterStream rfs = resource.adaptTo(ResourceFilterStream.class);
            rfs.setBranchSelector("[jcr:primaryType] == 'cq:Page'")
                    .setChildSelector("[jcr:content/sling:resourceType] == $type")
                    .addParam("type", pageResourceType)
                    .stream()
                    .map(r -> r.getChild(JcrConstants.JCR_CONTENT))
                    .forEach(this::updateProperty);
        }
    }

    private void updateProperty(Resource resultResource) {
        if (reAction == UpdateAction.ADD || reAction == UpdateAction.UPDATE) {
            addOrUpdateProp(resultResource);
        } else if (reAction == UpdateAction.DELETE) {
            removeProp(resultResource);
        }
    }

    private void removeProp(Resource resultResource) {
        try {
            ModifiableValueMap map = resultResource.adaptTo(ModifiableValueMap.class);
            propertyMap.entrySet().stream().forEach(r -> map.remove(r.getKey()));
            resultResource.getResourceResolver().commit();
            recordAction(resultResource.getPath(), reAction.name(), StringUtils.join(propertyMap));
        } catch (PersistenceException e) {
            LOGGER.error("Error occurred while persisting the property {}", e.getMessage());
        }
    }

    private void addOrUpdateProp(Resource resultResource) {
        try {
            ModifiableValueMap map = resultResource.adaptTo(ModifiableValueMap.class);
            propertyMap.entrySet().stream().forEach(r -> map.put(r.getKey(), r.getValue()));
            resultResource.getResourceResolver().commit();
            recordAction(resultResource.getPath(), reAction.name(), StringUtils.join(propertyMap));
        } catch (PersistenceException e) {
            LOGGER.error("Error occurred while persisting the property {}", e.getMessage());
        }
    }

    public enum ReportColumns {
        PATH, ACTION, DESCRIPTION
    }

    private void validateInputs() throws RepositoryException {
        if (sourceFile != null && sourceFile.getSize() > 0) {
            Spreadsheet sheet;
            try {
                sheet = new Spreadsheet(sourceFile, PROPERTY, PROPERTY_TYPE, PROPERTY_VALUE).buildSpreadsheet();
            } catch (IOException ex) {
                throw new RepositoryException("Unable to parse spreadsheet", ex);
            }

            if (!sheet.getHeaderRow().contains(PROPERTY) || !sheet.getHeaderRow().contains(PROPERTY_TYPE) || !sheet.getHeaderRow().contains(PROPERTY_VALUE)) {
                throw new RepositoryException(MessageFormat.format("Spreadsheet should have two columns, respectively named {0}, {1} and {2}", PROPERTY, PROPERTY_TYPE, PROPERTY_VALUE));
            }

            sheet.getDataRowsAsCompositeVariants().forEach(row -> {
                String propertyType = row.get(PROPERTY_TYPE).toString();
                if(StringUtils.equalsAnyIgnoreCase("String", propertyType)) {
                    propertyMap.put(row.get(PROPERTY).toString(), row.get(PROPERTY_VALUE).toString());
                } else if(StringUtils.equalsAnyIgnoreCase("Date", propertyType)) {
                    SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
                    propertyMap.put(row.get(PROPERTY).toString(), dt.format(row.get(PROPERTY_VALUE).toString()));
                } else if(StringUtils.equalsAnyIgnoreCase("Array", propertyType)) {
                    propertyMap.put(row.get(PROPERTY).toString(), row.get(PROPERTY_VALUE).toString().split(","));
                } else if(StringUtils.equalsAnyIgnoreCase("Long", propertyType)) {
                    Integer result = Optional.ofNullable(row.get(PROPERTY_VALUE).toString())
                            .filter(Objects::nonNull)
                            .map(Integer::parseInt)
                            .orElse(0);
                    propertyMap.put(row.get(PROPERTY).toString(), result);
                } else if(StringUtils.equalsAnyIgnoreCase("Binary", propertyType)) {
                    propertyMap.put(row.get(PROPERTY).toString(), Boolean.valueOf(row.get(PROPERTY_VALUE).toString()));
                }
            });
        }
    }

    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");
    }
}

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

Start MCP Process

You will see a new process called Property Update as shown below:

Click on the process

Property Updater process

Upload the excel sheet and provide search Path, Action and Resource Type to search for

Configure Process

Excel Sheet:

Upload the excel sheet attached and add the property name, data type and value as shown below:

Property Updater Excel

After executing the process, you can click the completed process and view the results as shown below:

View report
List of updated results with Action

You can also go to CRXDE and check the property getting added to the resources as shown below:

CRXDE results

You can also perform other options by selecting appropriate Action as shown below:

Action Dropdown

You can learn more about MCP here

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 Bulk move pages – MCP Renovator

Problem statement:

How to bulk move pages from one location to another location?

Requirement:

  1. Bulk move some of the pages from one location to another location
  2. Update the references of the page.
  3. Deactivate moved pages and delete
  4. Publish moved references
  5. Publish moved pages

Introduction:

Out of the box, AEM doesn’t support move option/operation for more than one selection of pages. However, we can use the MCP renovator process to bulk move the exiting pages from one location to another location.

Create an excel sheet containing the following columns:

  1. Source – source path of the page
  2. Destination – destination path and if you want to rename the page you do as well
bulk move excel sheet

For Eg:

Source: /content/wknd/language-masters/en/magazine

Destination: /content/wknd/language-masters/en/revenue

In the above example, the magazine page will be renamed to revenue

Go to the MCP process path http://{domain}}/apps/acs-commons/content/manage-controlled-processes.html, select Renovator process upload the excel sheet and use the following options as per your needs. If you select replication MCP queue will activate only references but won’t activate the moved pages.

Renovator process

Dry run the process to validate for any errors in the excel sheet and select the detailed report to view complete move related changes.

Process options selection

Excelsheet:

Renovator satisfy all 5 requirements but for the 6th requirement, you can use the below process to activate all the moved pages and you upload the same excel sheet and below process will only look into the destination column

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.

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;
        }
    }
}

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:

AEM Template Updater – Update the exiting template with a new template

Problem Statement:

How can I update an existing template with a new template? For example, If I want to update the 2nd/3rd level page template with a new template without moving child pages or reactivation.

Requirement:

  1. Update the Adventures page template from landing page template to adventure page template.
  2. Avoid moving child pages from current adventure page to new staging location
  3. Avoid bulk activation of child pages due to movement
Create duplicate page

Introduction:

Let’s try to solve the above issue manually:

Create a new page called adventures (which will be created has adventures0) using the adventure page template.

Copy all the child pages or move page by page from the current adventures page to the new adventure page.

Copy the child pages from source to destination

Delete the current adventures page and move the adventures0 page and rename it by removing 0

Move the page and renmae

Finally, click on manage publication to publish new adventures page and select all child pages to reactivate.

Include child pages before publishing

Let’s try to solve the above issue programmatically:

Create a new MCP process called TemplateTypeUpdaterFactory service class as shown below:

package com.mysite.mcp.process;

import org.osgi.service.component.annotations.Component;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class TemplateTypeUpdaterFactory extends ProcessDefinitionFactory<TemplateTypeUpdator> {
	@Override
	public String getName() {
		return "Template Type Updater";
	}
	@Override
	protected TemplateTypeUpdator createProcessDefinitionInstance() {
		return new TemplateTypeUpdator();
	}
}

Create TemplateTypeUpdator Implementation class as shown below:

package com.mysite.mcp.process;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.Description;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.WCMException;

public class TemplateTypeUpdator extends ProcessDefinition {

	private static final Logger LOGGER = LoggerFactory.getLogger(TemplateTypeUpdator.class);
	private static final String REPORT_NAME = "Page-Template-Updated";
	private static final String EXECUTING_KEYWORD = "Performs Page Template change";
	private static final String SLASH = "/";
	private static final String REPORT_SAVE_PATH = "/jcr:content/report";
	private static final String DESTINATION_COL = "duplicatepage";
	private static final String SOURCE_COL = "originalpage";
	final Map<String, String> channelPaths = Collections.synchronizedMap(new HashMap<>());
	private final List<EnumMap<ReportColumns, Object>> reportRows = new ArrayList<>();
	GenericReport genericReport = new GenericReport();

	@FormField(name = "Template Update", description = "Excel spreadsheet for performing channel type updator", component = FileUploadComponent.class, required = false)
	private RequestParameter sourceFile;

	@Override
	public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws LoginException, RepositoryException {
		validateInputs();
		genericReport.setName(REPORT_NAME);
		String desc = EXECUTING_KEYWORD;
		instance.defineCriticalAction("Updating Page", rr, this::createOrUpdatePage);
		instance.getInfo().setDescription(desc);
	}

	protected void createOrUpdatePage(ActionManager manager) {
		manager.deferredWithResolver(resourceResolver -> {
			try {
				for (Map.Entry<String, String> entry : channelPaths.entrySet()) {
					final PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
					Page destinationPage = resourceResolver.resolve(entry.getValue()).adaptTo(Page.class);
					Page sourcePage = resourceResolver.resolve(entry.getKey()).adaptTo(Page.class);
					pageManager.delete(sourcePage, true, false);
					JcrUtil.copy(resourceResolver.resolve(destinationPage.getPath() + SLASH + JcrConstants.JCR_CONTENT)
							.adaptTo(Node.class), sourcePage.adaptTo(Node.class), null);
					pageManager.delete(destinationPage, false, false);
					recordData(entry.getKey());
				}
				if (resourceResolver.hasChanges()) {
					resourceResolver.commit();
				}
			} catch (WCMException | RepositoryException e) {
				LOGGER.error("unable to create or update page {}", e.getMessage());
			}
		});
	}

	protected void recordData(String channelPagePath) {
		final EnumMap<ReportColumns, Object> row = new EnumMap<>(ReportColumns.class);
		row.put(ReportColumns.SERIAL, 1);
		row.put(ReportColumns.CONTENT_PATH, channelPagePath);
		reportRows.add(row);
	}

	private void validateInputs() throws RepositoryException {
		if (sourceFile != null && sourceFile.getSize() > 0) {
			Spreadsheet sheet;
			try {
				sheet = new Spreadsheet(sourceFile, SOURCE_COL, DESTINATION_COL).buildSpreadsheet();
			} catch (IOException ex) {
				throw new RepositoryException("Unable to parse spreadsheet", ex);
			}

			if (!sheet.getHeaderRow().contains(SOURCE_COL) || !sheet.getHeaderRow().contains(DESTINATION_COL)) {
				throw new RepositoryException(
						MessageFormat.format("Spreadsheet should have two columns, respectively named {0} and {1}",
								SOURCE_COL, DESTINATION_COL));
			}

			sheet.getDataRowsAsCompositeVariants().forEach(
					row -> channelPaths.put(row.get(SOURCE_COL).toString(), row.get(DESTINATION_COL).toString()));
		}
	}

	@Override
	public void storeReport(ProcessInstance instance, ResourceResolver rr)
			throws RepositoryException, PersistenceException {
		genericReport.setRows(reportRows, ReportColumns.class);
		genericReport.persist(rr, instance.getPath() + REPORT_SAVE_PATH);
	}

	@Override
	public void init() throws RepositoryException {
	}

	enum TemplateType {
		HOME_PAGE_TEMPLATE, EXPLORATORY_TOPIC_TEMPLATE, EXPLORATORY_TOOL_TEMPLATE, PROBLEM_SOLVING_TOPIC_TEMPLATE
	}

	public enum ReportColumns {
		SERIAL, CONTENT_PATH
	}

	public enum PublishMethod {
		@Description("Select this option to generate Generate Article Pages")
		CHANNEL_PAGES
	}
}

Create a new adventures page under any path using the adventure page template.

Create an excel sheet with the following columns:

  1. Duplicatepage column – new adventures path
  2. Originalpage column – exiting adventures page path
excel sheet columns

Upload this excel sheet as an input into the new process as shown below:

select the process
Upload the excel sheet

Once the process starts it will update the exiting page template with a new template and it will delete the duplicate page

Once the update is completed you can use quick publish the updated adventures page.

You can also use the same process to update single or multiple pages at once by adding multiple rows into the excel sheet.

Excelsheet:

AEM Groovy Console replacement? Extending ACS Commons Managed Control process (MCP)

Problem Statement:

What is MCP? How to extend MCP in your current project.

Requirement:

Extend MCP in your current project structure and add the relevant POM changes

Introduction:

MCP (Manage Controlled Processes) is both a dashboard for performing complex tasks and also 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.

The groovy console is an awesome tool to write short scripts to execute some of the content updates but due to increase security and safety issues, the AMS team is not allowing to install Groovy Console in the author/publish environment in Production and there is no other way we can run the groovy console on pubs. Hence ACS commons MCP would be the better replacement for running these kinds of bulk updates using the beautiful console provided by ACS Commons.

How to use MCP?

Process Manager: How to navigate and access ACS Commons MCP process manager

Tools: Tools that comes OOTB when you use MCP tools

Operations: How to use and maintain the MCP process

In order to extend the MCP process, create a separate module called mcp under your project directory and add the following dependencies as shown below:

Add the following code into all module pom.xml to embed this new module

Create the packages and start your first process:

For MCP process to pick up your new process, you need to create a service that implements ProcessDefinitionFactory and override getName method to provide the process name to show under the list of process.

MCP process list

createProcessDefinitionInstance to create a new instance of this process.

package com.mysite.mcp.process;

import org.osgi.service.component.annotations.Component;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class ExampleProcessFactory extends ProcessDefinitionFactory<ExampleProcess> {	
	@Override
	public String getName() {
		return "Example PRocess";
	}
	@Override
	protected ExampleProcess createProcessDefinitionInstance() {
		return new ExampleProcess();
	}
}

You can learn more regarding process definition here

Create Process Implementation:

Process implementation class extends ProcessDefinition and you can have multiple form fields to accept the author’s input.

Process Input fields

You can learn more about form fields here

You need to override:

init – to run initiallise the process

build process – define the Action and add the description of the process

storeReport – Basically used to save the action results under the specified path

package com.mysite.mcp.process;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import javax.jcr.RepositoryException;
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.fam.ActionManager;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.model.GenericReport;

public class ExampleProcess extends ProcessDefinition {
	
	private static final String REPORT_SAVE_PATH = "/jcr:content/report";
	private final List<EnumMap<ReportColumns, Object>> reportRows = new ArrayList<>();
    GenericReport genericReport = new GenericReport();
    
    @FormField(name = "Enter Text",
            description = "Example text to be reported",
            required = false,
            options = {"default=enter test text"})
    private String emapleText;
	
	@Override
    public void buildProcess(ProcessInstance instance, ResourceResolver rr) throws RepositoryException, LoginException {        
        genericReport.setName("exampleReport");
        instance.defineCriticalAction("Running Example", rr, this::reportExampleAction);
        instance.getInfo().setDescription("Executing Example Process");
    }
	
	protected void reportExampleAction(ActionManager manager) {
		record(emapleText);
	}
	
	private void record(String emapleText) {
		 final EnumMap<ReportColumns, Object> row = new EnumMap<>(ReportColumns.class);
		 row.put(ReportColumns.SERIAL, 1);
	        row.put(ReportColumns.TEXT, emapleText);
	        reportRows.add(row);
	}

	@Override
    public void storeReport(ProcessInstance instance, ResourceResolver rr) throws RepositoryException, PersistenceException {
        genericReport.setRows(reportRows, ReportColumns.class);
        genericReport.persist(rr, instance.getPath() + REPORT_SAVE_PATH);
    }

    @Override
    public void init() throws RepositoryException {
    }
    
    public enum ReportColumns {
        SERIAL, TEXT
    }
}

you can learn more about the process lifecycle here

Once the execution is complete we can save and see the report results:

Running Example process
Results page showing Serial and text for the input
References:

https://adobe-consulting-services.github.io/acs-aem-commons/features/mcp/index.html