AEM Performance Optimization Activation/Replication

Problem Statement:

AEM Bulk Replication allows you to activate a series of pages and/or assets.

How can we make sure workflow won’t impact AEM performance (CPU or Heap memory) / throttle the system?

Introduction:

AEM bulk replication or activation is performed on a series of pages and/or assets. We usually perform bulk replication on tree structures or lists of paths.

Use case:

  1. MSM (Multi-site management) – rolling out a series of pages or site
  2. Editable template – add/remove new components on template structure and activate existing pages
  3. Bulk Asset ingests into the system
  4. Bulk redirect//vanity path update

ACS Commons Throttled Task Runner is built on Java management API for managing and monitoring the Java VM and can be used to pause tasks and terminate the tasks based on stats.

Throttled Task Runner (a managed thread pool) provides a convenient way to run many AEM-specific activities in bulk it basically checks for the Throttled Task Runner bean and gets current running stats of the actual work being done.

OSGi Configuration:

The Throttled Task Runner is OSGi configurable, but please note that changing configuration while work is being processed results in resetting the worker pool and can lose active work.

Throttled Task Runner OSGi

Max threads: Recommended not to exceed the number of CPU cores. Default 4.

Max CPU %: Used to throttle activity when CPU exceeds this amount. Range is 0..1; -1 means disable this check.

Max Heap %: Used to throttle activity when heap usage exceeds this amount. Range is 0..1; -1 means disable this check.

Cooldown time: Time to wait for CPU/MEM cooldown between throttle checks (in milliseconds)

Watchdog time: Maximum time allowed (in ms) per action before it is interrupted forcefully.

JMX MBeans

Throttled Task Runner MBean

This is the core worker pool. All action managers share the same task runner pool, at least in the current implementation. The task runner can be paused or halted entirely, throwing out any unfinished work.

Throttled task runner JMX
Throttled task runner JMX

How to use ACS Commons throttled task runner

Add the following dependency to your pom

Create a custom service or servlet as shown below:

Throttled Replication

inside the custom workflow process method check for the Low CPU and Low memory before starting your task to avoid performance impact on the system.

For bulk replication (publish/unpublish/delete) assets or pages, please refer to the AEM Operation Replication Tool

For best practices on the AEM servlet please refer to the link.

AEM Performance Optimization Workflow

Problem Statement:

AEM Workflows allow you to automate a series of steps that are performed on (one or more) pages and/or assets.

How can we make sure workflow won’t impact AEM performance (CPU or Heap memory) / throttle the system?

Introduction:

AEM Workflows allow you to automate a series of steps that are performed on (one or more) pages and/or assets.

For example, when publishing, an editor has to review the content – before a site administrator activates the page. A workflow that automates this example notifies each participant when it is time to perform their required work:

  1. The author applies the workflow to the page.
  2. The editor receives a work item that indicates that they are required to review the page content. When finished, they indicate that their work item is complete.
  3. The site administrator then receives a work item that requests the activation of the page. When finished, they indicate that their work item is complete.

Typically:

  • Content authors apply workflows to pages as well as participate in workflows.
  • The workflows that you use are specific to the business processes of your organization.

ACS Commons Throttled Task Runner is built on Java management API for managing and monitoring the Java VM and can be used to pause tasks and terminate the tasks based on stats.

Throttled Task Runner (a managed thread pool) provides a convenient way to run many AEM-specific activities in bulk it basically checks for the Throttled Task Runner bean and gets current running stats of the actual work being done.

OSGi Configuration:

The Throttled Task Runner is OSGi configurable, but please note that changing configuration while work is being processed results in resetting the worker pool and can lose active work.

Throttled task runner OSGi

Max threads: Recommended not to exceed the number of CPU cores. Default 4.

Max CPU %: Used to throttle activity when CPU exceeds this amount. Range is 0..1; -1 means disable this check.

Max Heap %: Used to throttle activity when heap usage exceeds this amount. Range is 0..1; -1 means disable this check.

Cooldown time: Time to wait for CPU/MEM cooldown between throttle checks (in milliseconds)

Watchdog time: Maximum time allowed (in ms) per action before it is interrupted forcefully.

JMX MBeans

Throttled Task Runner MBean

This is the core worker pool. All action managers share the same task runner pool, at least in the current implementation. The task runner can be paused or halted entirely, throwing out any unfinished work.

Throttled task runner JMX
Throttled task runner JMX

How to use ACS Commons throttled task runner

Add the following dependency to your pom

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

Create a custom workflow and call the service as shown below:

Throttled Workflow

inside the custom workflow process method check for the Low CPU and Low memory before starting your task to avoid performance impact on the system.

For all the bulk workflow executions on pages/assets, it’s recommended to use ACS commons bulk workflow.

For best practices on workflow please refer to the link

AEM Performance Optimization Scheduler

Problem Statement:

AEM Schedulers are commonly used to run bulk tasks at an off time (nonbusiness hours) and some tasks are run periodically to fetch results cached and fetched by the front end.

How can we make sure bulk tasks won’t impact AEM performance (CPU or Heap memory) / throttle the system?

Introduction:

In Computer Software, Scheduling is a paradigm of planning for the execution of a task at a certain point in time and it can be broadly classified into two types:

1. Scheduled Task – executing once at a particular future point in time

2. Frequent scheduling – repeat periodically at a fixed interval

The use case for the scheduler:

1. Sitemap generation

2. Synching product data from AEM Commerce

3. Fetch DB content and place it in the repository to be picked up by frontend and in turn cached in Dispatcher

4. Fetch Stats or reports and place them in the repository to be picked up by frontend and in turn cached in Dispatcher

ACS Commons Throttled Task Runner is built on Java management API for managing and monitoring the Java VM and can be used to pause tasks and terminate the tasks based on stats.

Throttled Task Runner (a managed thread pool) provides a convenient way to run many AEM-specific activities in bulk it basically checks for the Throttled Task Runner bean and gets current running stats of the actual work being done.

OSGi Configuration:

The Throttled Task Runner is OSGi configurable, but please note that changing configuration while work is being processed results in resetting the worker pool and can lose active work.

Throttled task runner OSGi

Max threads: Recommended not to exceed the number of CPU cores. Default 4.

Max CPU %: Used to throttle activity when CPU exceeds this amount. Range is 0..1; -1 means disable this check.

Max Heap %: Used to throttle activity when heap usage exceeds this amount. Range is 0..1; -1 means disable this check.

Cooldown time: Time to wait for CPU/MEM cooldown between throttle checks (in milliseconds)

Watchdog time: Maximum time allowed (in ms) per action before it is interrupted forcefully.

JMX MBeans

Throttled Task Runner MBean

This is the core worker pool. All action managers share the same task runner pool, at least in the current implementation. The task runner can be paused or halted entirely, throwing out any unfinished work.

Throttled task runner JMX

How to use ACS Commons throttled task runner

Add the following dependency to your pom

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

Create a scheduler and call the service as shown below:

Throttled Scheduler call

inside the run, method check for the Low CPU and Low memory before starting your task to avoid performance impact on the system

For the best practices on AEM Scheduler please refer to the link

Retry Utils in AEM using Java Streams

Problem Statement:

  1. How to retry a connection?
  2. Retry posting the data at least some time until the response is 200
  3. Retry any OAK operations, when the exception occurs

Introduction:

Usually, with respect to AEM, we don’t have Retry Utils which can retry the particular operation whenever an exception occurred.

If we are doing multiple transactions on the AEM repository, especially on a particular node like updating properties or updating references, the OAK operation would through exceptions like your operation is blocked by another operation or invalid modification.

If we are connecting to external services through REST API and the connection has failed or timeout and if we want to connect to the external system then we don’t have the option to retry out of the box

Create a Retry Utils as shown below:

Retry on Exception:

package com.test.utils;

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

public class RetryOnException {
    private static final Logger log = LoggerFactory.getLogger(RetryOnException.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 counter = 0; counter < maxTimes; counter++) {
            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(), (counter + 1), maxTimes, e);
            }
            try {
                Thread.sleep(intervalWait);
            } catch (InterruptedException wakeAndAbort) {
                break;
            }
        }
        throw thrown;
    }
}

The above Util can be used in any code as shown below and the retry will happen only when the exception occurs during operations

package com.test.utils;

import java.util.concurrent.atomic.AtomicInteger;
import org.aarp.www.mcp.utils.RetryOnException;

public class ExampleOne {
	public static void main(String[] args) {
		AtomicInteger atomicCounter = new AtomicInteger(0);
		try {
			RetryOnException.withRetry(3, 500, () -> {
				if(atomicCounter.getAndIncrement() < 2) {
					System.out.println("Retring count with Exception" + atomicCounter.get());
					throw new Exception("Throwing New Exception to test");
				} else {
					System.out.println("Retring count without Exception " + atomicCounter.get());
				}

			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
Exception Result

Retry on the condition:

package com.test.utils;

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

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

    public static interface CallToRetry {
        boolean 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 counter = 0; counter < maxTimes; counter++) {
            try {
                boolean status = call.process();
                if(status) {
                	return true;
                }                
            } catch (Exception e) {
                thrown = e;
                log.info("Encountered failure on {} due to {}, attempt retry {} of {}", call.getClass().getName() , e.getMessage(), (counter + 1), maxTimes, e);
            }
            try {
                Thread.sleep(intervalWait);
            } catch (InterruptedException wakeAndAbort) {
                break;
            }
        }
        throw thrown;
    }
}

The above Util can be used to retry based on the condition like if the connection is successful or the response code is 200

package com.test.utils;

import java.util.concurrent.atomic.AtomicInteger;
import org.aarp.www.mcp.utils.RetryOnCondition;

public class ExampleTwo {
	public static void main(String[] args) {
		AtomicInteger atomicCounter = new AtomicInteger(0);
		try {
			RetryOnCondition.withRetry(3, 500, () -> onCondition(atomicCounter));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private static boolean onCondition(AtomicInteger atomicCounter) throws Exception {		
		if(atomicCounter.getAndIncrement() < 2) {
			System.out.println("Retring count with Condition false " + atomicCounter.get());
			return false;
		} else {
			System.out.println("Retring count with Condition true " + atomicCounter.get());
			return true;
		}
	}
}
On Condition result

Retry n number of times:

package org.aarp.www.mcp.utils;

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

public class RetryNTimes {
    private static final Logger log = LoggerFactory.getLogger(RetryNTimes.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 counter = 0; counter < maxTimes; counter++) {
            try {
                call.process();
                if(counter == (maxTimes-1)) {
                	return true;
                }                
            } catch (Exception e) {
                thrown = e;
                log.info("Encountered failure on {} due to {}, attempt retry {} of {}", call.getClass().getName() , e.getMessage(), (counter + 1), maxTimes, e);
            }
            try {
                Thread.sleep(intervalWait);
            } catch (InterruptedException wakeAndAbort) {
                break;
            }
        }
        throw thrown;
    }
}

The above Util can be used in operations like revalidating the results, for example, any query results can be validated.

package com.test.utils;

import java.util.concurrent.atomic.AtomicInteger;
import org.aarp.www.mcp.utils.RetryNTimes;

public class ExampleThree {
	public static void main(String[] args) {
		AtomicInteger atomicCounter = new AtomicInteger(0);
		try {
			RetryNTimes.withRetry(3, 500, () -> {
				System.out.println("Retrying 3 times count " + atomicCounter.getAndIncrement());
			});
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
Retying umtil retry count reached result

Perfect Content backup Package in AEM

Problem statement:

Create a perfect package in AEM, whenever we want to create any package in AEM we provide the content paths in the package filter we get only content pages. But what about images and reference pages? What about experience fragment pages or XF page-related context hub variations?

Introduction:

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>

In order to create a perfect content package in AEM, please create an MCP as shown below:

Create Process Definition factory – PackageCreatorFactory

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.apache.jackrabbit.vault.packaging.Packaging;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;
import com.adobe.acs.commons.packaging.PackageHelper;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class PackageCreatorFactory extends ProcessDefinitionFactory<PackageCreator> {
	
	@Reference
    private PackageHelper packageHelper;
	
	@Reference
    private Packaging packaging;
	
    @Override
    public String getName() {
        return "Package Creator";
    }

    @Override
    protected PackageCreator createProcessDefinitionInstance() {
        return new PackageCreator(packageHelper, packaging);
    }
}

Create Process Definition implementation – PackageCreator

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 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.form.TextareaComponent;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.adobe.acs.commons.mcp.model.ManagedProcess;
import com.adobe.acs.commons.packaging.PackageHelper;
import com.mysite.mcp.utils.ReportRetryUtils;
import com.mysite.mcp.visitors.ContentVisitor;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.packaging.*;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.jetbrains.annotations.NotNull;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Package Creator to create fully functional package for the given path
 */
public class PackageCreator extends ProcessDefinition {

	private final GenericReport report = new GenericReport();	
	
	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";
	private static final String REPORT_NAME = "Package_Creator";
    
    @FormField(name = "Page Paths",
            description = "Comma-separated list of components to exclude",
            required = false,
            component = TextareaComponent.class,
            options = {"default=/content/"})
    private String pagePaths;
    
    @FormField(name = "Package Name",
            description = "Custom package name",
            required = false,
            options = {"default=backing up"})
    private String packageName;
    
    @FormField(name = "Package Description",
            description = "Custom package Description",
            required = false,
            options = {"default=Package created by package creator"})
    private String packageDescription;

    ManagedProcess instanceInfo;
    private PackageHelper packageHelper;
    private Packaging packaging;

    public PackageCreator(PackageHelper packageHelper, Packaging packaging) {
    	this.packageHelper = packageHelper;
    	this.packaging = packaging;
	}

	@Override
    public void init() throws RepositoryException {
    }

    @Override
    public void buildProcess(ProcessInstance instance, ResourceResolver resourceResolver) throws LoginException, RepositoryException {
    	instanceInfo = instance.getInfo();
    	instance.getInfo().setDescription("Generating package");
    	instance.defineAction("Preparing package", resourceResolver, this::createPackage); 
    	report.setName(REPORT_NAME);
    }
    
    protected void createPackage(ActionManager manager) {
    	manager.deferredWithResolver(this::collectReferences); 
    }
    
    private void collectReferences(ResourceResolver resourceResolver) throws Exception {
    	Set<String> contentPaths = new HashSet<>();
    	Set<String> xfPaths = new HashSet<>();
    	Set<String> damPaths = new HashSet<>();    	
    	ContentVisitor contentVisitor =  new ContentVisitor();
    	Set<String> pages = new HashSet<>(Arrays.asList(pagePaths.split(","))).stream().map(r -> r + "/jcr:content").collect(Collectors.toSet());
    	iterateContentPaths(resourceResolver, contentPaths, xfPaths, damPaths, contentVisitor, pages);
    	damPaths.addAll(iterateContent(resourceResolver, contentVisitor, damPaths));
    	ReportRetryUtils.withRetry(2, 300, () -> xfPaths.addAll(iterateContent(resourceResolver, contentVisitor, xfPaths)));    	    	
    	Set<String> allPaths = Stream.of(pages, contentPaths, xfPaths, damPaths).flatMap(Collection::stream).collect(Collectors.toSet());
        packagePaths(resourceResolver, allPaths);
    }

	private Set<String> iterateContent(ResourceResolver resourceResolver, ContentVisitor contentVisitor, Set<String> pages) {
		Set<String> interimPaths = new HashSet<>();
		for(String page : pages) {
			contentVisitor.accept(resourceResolver.resolve(page));
			interimPaths.addAll(contentVisitor.getContentPaths());
			interimPaths.addAll(contentVisitor.getDamPaths());
			interimPaths.addAll(contentVisitor.getXfPaths());
		}
		return interimPaths;
	}

	private void iterateContentPaths(ResourceResolver resourceResolver, Set<String> contentPaths, Set<String> xfPaths,
			Set<String> damPaths, ContentVisitor contentVisitor, Set<String> pages) {
			for(String page : pages) {
				contentVisitor.accept(resourceResolver.resolve(page));
				contentPaths.addAll(contentVisitor.getContentPaths());
				xfPaths.addAll(contentVisitor.getXfPaths());
				damPaths.addAll(contentVisitor.getDamPaths());
			}
	}

	private void packagePaths(ResourceResolver resourceResolver, Set<String> allPaths)
			throws IOException, RepositoryException, PackageException {
		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);
    	Set<@NotNull Resource> packageResources = allPaths.stream().map(resourceResolver::resolve).collect(Collectors.toSet());
    	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);
            recordAction(jcrPackage.getNode().getPath(), "Built", "Package Built is successful");
    	}		
	}


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

Add the Utils called ReportRetryUtils as shown below:

package com.mysite.mcp.utils;

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

public class ReportRetryUtils {
    private static final Logger log = LoggerFactory.getLogger(ReportRetryUtils.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 counter = 0; counter < maxTimes; counter++) {
            try {
                call.process();
                if(counter == (maxTimes-1)) {
                	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 class called ContentVistor as shown below:

package com.mysite.mcp.visitors;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.regex.Pattern;

public class ContentVisitor extends AbstractResourceVisitor {
	
	private static final String CONTENT_SLASH = "/content/";
	private static final String CONTENT_DAM_SLASH = "/content/dam/";
	private static final String CONTENT_XF_SLASH = "/content/experience-fragments/";
	
	Pattern htmlPattern = Pattern.compile(".*\\<[^>]+>.*", Pattern.DOTALL);
	
	Set<String> contentPaths = new HashSet<>();
	Set<String> xfPaths = new HashSet<>();
	Set<String> damPaths = new HashSet<>();
	
	@Override
	public final void accept(final Resource resource) {
		if (null != resource && !ResourceUtil.isNonExistingResource(resource)) {
			final ValueMap properties = resource.adaptTo(ValueMap.class);
			final String primaryType = properties.get(ResourceResolver.PROPERTY_RESOURCE_TYPE, StringUtils.EMPTY);
			if(StringUtils.isNoneEmpty(primaryType) || StringUtils.startsWith(resource.getPath(), "/content/dam/content-fragments")){
				visit(resource);
			}
			this.traverseChildren(resource.listChildren());
		}
	}

	@Override
	protected void traverseChildren(final @NotNull Iterator<Resource> children) {
		while (children.hasNext()) {
			final Resource child = children.next();
			accept(child);
		}
	}

	@Override
	protected void visit(final @NotNull Resource resource) {
		resource.getValueMap().entrySet().forEach(property -> {
			Object prop = property.getValue();
			if (prop.getClass() == String[].class) {
				List<String> propertyValue = Arrays.asList((String[]) prop);
				if (!propertyValue.isEmpty()) {							
					propertyValue.stream().filter(s -> StringUtils.isNotEmpty(s) && StringUtils.startsWith(s, CONTENT_SLASH) && !htmlPattern.matcher(s).matches()).forEach(this::populateValue);
				}
			} else if (prop.getClass() == String.class) {
				String propertyValue = (String) prop;
				if (StringUtils.isNotEmpty(propertyValue) && StringUtils.startsWith(propertyValue, CONTENT_SLASH) && !htmlPattern.matcher(propertyValue).matches()) {
					populateValue(propertyValue);
				}
			}
		});
	}

	private void populateValue(String value) {
		if(StringUtils.startsWith(value, CONTENT_DAM_SLASH)) {
			damPaths.add(value);
		} else if(StringUtils.startsWith(value, CONTENT_XF_SLASH)) {
			xfPaths.add(value+"/jcr:content");
		} else {
			contentPaths.add(StringUtils.substringBeforeLast(value, ".html")+"/jcr:content");		
		}
	}

	public Set<String> getContentPaths() {
		return contentPaths;
	}

	public Set<String> getXfPaths() {
		return xfPaths;
	}

	public Set<String> getDamPaths() {
		return damPaths;
	}	
}

Once the 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

Package Creator Process

Provide all the page paths as a comma separated values, package name, and descriptions before starting the process

Create Package Process Definition

Once the package is created in the results you can find the package location as shown below:

Create Package reutlt

As can see in the filter, it has packaged the content path along with related images and header and footer XF paths as well

Backup package in package manager view

Access CRX Package Manager in PROD

Problem statement:

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

User cases:

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

Introduction:

Packages enable the importing and exporting of repository content. For example, you can use packages to install new functionality, transfer content between instances, and back up repository content.

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.

In order to access packages in AEM:

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 – PackageHandlerFactory

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.apache.jackrabbit.vault.packaging.Packaging;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.adobe.acs.commons.mcp.ProcessDefinitionFactory;

@Component(service = ProcessDefinitionFactory.class, immediate = true)
public class PackageHandlerFactory extends ProcessDefinitionFactory<PackageHandler> {
	
    @Reference
    private Packaging packaging;
	
    @Override
    public String getName() {
        return "Package Handler";
    }

    @Override
    protected PackageHandler createProcessDefinitionInstance() {
        return new PackageHandler(packaging);
    }
}

Create Process Definition implementation – PackageHandler

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.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import com.mysite.mcp.utils;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.JcrPackageManager;
import org.apache.jackrabbit.vault.packaging.PackageException;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.Packaging;
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.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.RadioComponent;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.adobe.acs.commons.mcp.model.ManagedProcess;

/**
 * Package Creator to create fully functional package for the given path
 */
public class PackageHandler extends ProcessDefinition {

	private static final Logger LOGGER = LoggerFactory.getLogger(PackageHandler.class);
	private final GenericReport report = new GenericReport();
	private static final String REPORT_NAME = "Package_Action_Performed";
	
    public enum PackAction {
        upload, install, upload_install, build, delete
    }

	@FormField(name = "Upload Package", description = "Upload JCR Package", component = FileUploadComponent.class)
	public transient InputStream inputPackage = null;
	
	@FormField(name = "Package Name",
            description = "Package Name to be Installed",
            required = false,
            options = {"default=Package Name"})
    private String packageName;
	
	@FormField(name = "Package Group",
            description = "Package Group to be Installed",
            required = false,
            options = {"default=Package Group"})
    private String packageGroup;
	
	@FormField(name = "Package Version",
            description = "Package Version to be Installed",
            required = false,
            options = {"default=Package Version"})
    private String packageVersion;
	
    @FormField(
            name = "Package Option",
            description = "Option is mandatory to be picked",
            component = RadioComponent.EnumerationSelector.class,
            options = {"default=upload", "vertical"}
    )
    protected transient PackAction packageOption = PackAction.upload;

	ManagedProcess instanceInfo;
	private Packaging packaging;

	public PackageHandler(Packaging packaging) {
		this.packaging = packaging;
	}

	@Override
	public void init() throws RepositoryException {
	}

	@Override
	public void buildProcess(ProcessInstance instance, ResourceResolver resourceResolver)
			throws LoginException, RepositoryException {
		instanceInfo = instance.getInfo();
		instance.getInfo().setDescription("Package "+ packageOption);
		switch (packageOption) {
		case install:
			instance.defineAction("Installing Package", resourceResolver, this::installPackage);
			break;
		case upload_install:
			instance.defineAction("Uploading Package", resourceResolver, this::uploadAndInstallPackage);
			break;
		case upload:
			instance.defineAction("Uploading and Installing Package", resourceResolver, this::uploadPackage);
			break;
		case build:
			instance.defineAction("Building Package", resourceResolver, this::buildPackage);
			break;
		case delete:
			instance.defineAction("Deleting Package", resourceResolver, this::deletePackage);
			break;
		default: 
			break;			
		}
		report.setName(REPORT_NAME);
	}

	protected void uploadPackage(ActionManager manager) {
		manager.deferredWithResolver(this::uploadPack);
	}
	
	protected void installPackage(ActionManager manager) {
		manager.deferredWithResolver(this::installPack);
	}
	
	protected void uploadAndInstallPackage(ActionManager manager) {
		manager.deferredWithResolver(this::uploadAndInstallPack);
	}

	protected void buildPackage(ActionManager manager) {
		manager.deferredWithResolver(this::buildPack);
	}
	
	protected void deletePackage(ActionManager manager) {
		manager.deferredWithResolver(this::deletePack);
	}
	
	private void uploadPack(ResourceResolver resourceResolver) throws RepositoryException, IOException {
		try (InputStream inputPack = inputPackage) {
			if (null != inputPack) {
				final JcrPackageManager packageManager = packaging
						.getPackageManager(resourceResolver.adaptTo(Session.class));
				JcrPackage jcrPackage = packageManager.upload(inputPack, true);
				recordAction(jcrPackage.getNode().getPath(), "Uploaded", "Package Upload is successful");
			}
		}
	}
	
	private void installPack(ResourceResolver resourceResolver) {
		installPackage(resourceResolver, packageGroup, packageName, packageVersion, ImportMode.REPLACE, AccessControlHandling.IGNORE);
	}
	
	private void buildPack(ResourceResolver resourceResolver) {
		buildPackage(resourceResolver, packageGroup, packageName, packageVersion);
	}
	
	private void deletePack(ResourceResolver resourceResolver) {
		deletePackage(resourceResolver, packageGroup, packageName, packageVersion);
	}
	
	private void uploadAndInstallPack(ResourceResolver resourceResolver) throws RepositoryException, IOException {
		try (InputStream inputPack = inputPackage) {
			if (null != inputPack) {
				final JcrPackageManager packageManager = packaging
						.getPackageManager(resourceResolver.adaptTo(Session.class));
				try(JcrPackage jcrPackage = packageManager.upload(inputPack, true)) {
					recordAction(jcrPackage.getNode().getPath(), "Uploaded", "Package Upload is successful");
					installPackage(jcrPackage, ImportMode.REPLACE, AccessControlHandling.IGNORE);
				}				
			}
		}
	}
	
   public boolean installPackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version, final ImportMode importMode,
           final AccessControlHandling aclHandling) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
           final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
           jcrPackage.install(opts);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Install", "Package Installation is successful");
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean buildPackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
    	   packageManager.assemble(jcrPackage, null);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Build", "Package Building is successful");
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean deletePackage(ResourceResolver resourceResolver, final String groupName,
           final String packageName, final String version) {
       boolean result;
       final JcrPackageManager packageManager = packaging.getPackageManager(resourceResolver.adaptTo(Session.class));
       final PackageId packageId = new PackageId(groupName, packageName, version);
       try(JcrPackage jcrPackage = packageManager.open(packageId)) {
    	   packageManager.remove(jcrPackage);
           result = true;
           recordAction(jcrPackage.getNode().getPath(), "Delete", "Package Deletion is successful");
       } catch (RepositoryException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }
   
   public boolean installPackage(JcrPackage jcrPackage, final ImportMode importMode, final AccessControlHandling aclHandling) {
       boolean result;
       try {
           final ImportOptions opts = VltUtils.getImportOptions(aclHandling, importMode);
           jcrPackage.install(opts);
           result = true;
       } catch (RepositoryException | PackageException | IOException e) {
           LOGGER.error("Could not install package", e);
           result = false;
       }
       return result;
   }

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

Add the Utils class called – VltUtils

package com.mysite.mcp.utils;

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

/**
 * Utility class for creating vlt filters and import/export options
 */
public class VltUtils {
	
	public static ImportOptions getImportOptions(AccessControlHandling aclHandling, ImportMode importMode) {
		ImportOptions opts = new ImportOptions();
		if (aclHandling != null) {
			opts.setAccessControlHandling(aclHandling);
		} else {
			// default to overwrite
			opts.setAccessControlHandling(AccessControlHandling.OVERWRITE);
		}
		if (importMode != null) {
			opts.setImportMode(importMode);
		} else {
			// default to update
			opts.setImportMode(ImportMode.UPDATE);
		}

		return opts;
	}
}

Once the 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 Package Handler as shown below:

Click on the process

Package Handler process

Use the browser to upload the package and use the below options to perform any package-related operations:

  1. Upload
  2. Install
  3. Build
  4. Upload and Install
  5. Delete
Package Handler Option

Use the Package field to browse and upload the package, no need to fill in other options

To Build, Install or delete an existing package please fill in the package name, group, and versions

You can also download the built package by visiting the result page and hitting the path directly over the browser as shown below:

Upload a package and once the packge is installed you will be able to veiw the process result

Process Result page

copy the etc/package result and hot the donain/etc/package to download the package

Package downloaded

Delete 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 Asset (Repo) Cleanup – ACS Commons Renovator

Problem Statement:

How to clean up the growing repo? How to safely delete all the unwanted assets and pages.

Requirement:

Find the references of all the assets and pages and clean up unreferenced assets and unwanted pages.

Introduction:

You can call the below process as asset, pages references report.

Usually, with growing repo size, we usually do logs rotation and archiving, we also do some compactions (Revision cleanup).

What if we could remove some of the deactivated and unreferenced assets or pages?

How to find references of assets or pages?

Go to the following url https://{domain}/apps/acs-commons/content/manage-controlled-processes.html and click on Start Process and select Renovator process as shown below:

Start Process
Renovator Process

I am trying to check the references of all the assets under the following path:

Source path: /content/dam/wknd/en/activities

And select some random path into the Destination path: /content/dam/wknd/en/magazine

And please do make sure to check the Dry run and Detailed Report checkboxes, if not checked all the assets will be moved to the new folder i.e, /content/dam/wknd/en/magazine

Process fields selections

Once you start the process you would see the process take some time to run and you can click on the process and open the view or download the excel report as shown:

Process result page
View the results popup

Once downloaded delete the following columns:

Remove unwanted columns

You can see some of the rows have empty references and if you think these assets are no more required then you can remove them

Unreferenced rows

How to remove the unreferenced assets or pages safely?

Use the following resource to learn more about:

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

You can run through the above steps on any of the folders and please make sure to avoid running on root folders or pages like: /content/dam or /content or home pages because would slow down the servers

For more information on how to use the renovator process for

AEM Bulk move pages – MCP

AEM Create package using a list of paths

Problem Statement:

How to create a package using a list of paths?

Requirement:

Create a package for a list of paths using the ACS commons query package tool

Introduction:

Usually, when we try to create a package of content from the lowers or prod environment, we provide a list of pages we are trying to create a package for by going into the edit.

Add filters

What about the assets related to those pages? Again, you need to get the list of assets and edit the package.

Is there a way we can create a package using the ACS Commons tool?

The simple answer is Yes.

Go to using http://{domain}/miscadmin#/etc/acs-commons/packagers and create a page using Query Packager template as shown:

Create Query Packager page

Open the page and edit the configuration and fill the fileds

  1. Add package details
  2. Select query language has List
  3. Paste the all the paths
  4. Make sure if you want only the page in the list then add “/jcr:content” else it will get child pages as well
  5. Save and click on create package
  6. If you want package from publisher env, then activate the page and go to the miscadmin path and click on the create package
Fill the Query configuration

Unfortunately, it won’t show if the package is created or not but if you go to crx/packmgr url you will see the package

example package