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

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

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

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

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