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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s