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

Problem Statement:

How can I update an existing template with a new template without moving child pages or reactivation it?

Requirement:

This article discusses how to update an existing template with a new template in AEM without moving child pages or reactivation. The article provides two methods to achieve this goal: a manual method and a programmatic method. The programmatic method uses Manage Controlled Processes (MCP), which is both a dashboard and a rich API for defining complex tasks as process definitions. The article provides a step-by-step guide to updating the template type programmatically.

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

Introduction:

Let’s try to solve the above issue manually:

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

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

Copy the child pages from source to destination

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

Move the page and rename

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

Include child pages before publishing

Let’s try to solve the above issue programmatically:

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

Add the following maven dependency to your pom to extend MCP

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

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

package com.mysite.mcp.process;

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

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

Create TemplateTypeUpdator Implementation class as shown below:

package com.mysite.mcp.process;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.acs.commons.data.Spreadsheet;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.Description;
import com.adobe.acs.commons.mcp.form.FileUploadComponent;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.model.GenericReport;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.WCMException;

public class TemplateTypeUpdator extends ProcessDefinition {

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

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

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

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

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

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

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

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

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

	@Override
	public void init() throws RepositoryException {
	}

	enum TemplateType {
		HOME_PAGE_TEMPLATE, EXPLORATORY_TOPIC_TEMPLATE, EXPLORATORY_TOOL_TEMPLATE, PROBLEM_SOLVING_TOPIC_TEMPLATE
	}

	public enum ReportColumns {
		SERIAL, CONTENT_PATH
	}

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

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

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

Create an Excel sheet with the following columns:

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

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

select the process
Upload the Excel sheet

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

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

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

Excel sheet:

Extending ACS Commons Managed Control process (MCP) as a Replacement for AEM Groovy Console

Problem Statement:

With increasing security and safety concerns, AEM Groovy Console is not allowed to be installed in the author/publish environment in Production. As a result, users require an alternative tool to execute short scripts for content updates.

Requirement:

The Groovy Console has been a preferred tool for executing short scripts for content updates in Adobe Experience Manager (AEM), but due to security and safety concerns, the console is not installed in the author/publish environment in Production. This article explores using the ACS Commons Managed Control Process (MCP) as a better alternative for running bulk updates with the provided console. It provides an introduction to MCP and explains how to extend it in a current project structure and make relevant POM changes. It also covers tools that come out of the box with MCP, how to use and maintain them, and how to create a new process and form fields with MCP.

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

Introduction:

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

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

How to use MCP?

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

Tools: Tools that come to OOTB when you use MCP tools

Operations: How to use and maintain the MCP process

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

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

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

Create the packages and start your first process:

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

MCP process list

createProcessDefinitionInstance to create a new instance of this process.

package com.mysite.mcp.process;

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

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

You can learn more regarding process definition here

Create Process Implementation:

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

Process Input fields

You can learn more about form fields here

You need to override:

init – to run initialize the process

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

store report – Basically used to save the action results under the specified path

package com.mysite.mcp.process;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import javax.jcr.RepositoryException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.ResourceResolver;
import com.adobe.acs.commons.fam.ActionManager;
import com.adobe.acs.commons.mcp.ProcessDefinition;
import com.adobe.acs.commons.mcp.ProcessInstance;
import com.adobe.acs.commons.mcp.form.FormField;
import com.adobe.acs.commons.mcp.model.GenericReport;

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

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

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

you can learn more about the process lifecycle here

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

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

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