Content Fragments in Depth. CF Management Workflow. Part 3.3
Table of contents
We’ve explored content fragments and workflows in AEM with json libraries in our last article. Today, we’re looking at the flow that comprises CFs rollout, Proxy Pages creation and subsequent pages rollout, CF’s and Pages replication and flushing cache. Let’s jump right in!
Content Fragments Rollout
In this article, the first workflow that is being called is
var ROLLOUT_WORKFLOW_CF_MODEL_ID = "/var/workflow/models/site/site-cf-rollout-workflow";
It has 2 steps: language copy creation and links localization.
Creating Language Copy
com.wcm.site.workflow.ContentFragmentSmartUpdateVariationLanguageCopyProcess:
package com.wcm.site.workflow;
import com.adobe.acs.commons.util.WorkflowHelper;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowData;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.day.cq.dam.commons.util.DamLanguageUtil;
import com.day.cq.wcm.api.PageManagerFactory;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.wcm.site.localization.PathContext;
import com.wcm.site.util.*;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import java.io.IOException;
import java.util.*;
import static com.day.cq.commons.jcr.JcrConstants.*;
import static com.wcm.site.services.impl.CFProxyReferenceUpdateConsumer.PROXY_PAGE_REFERENCE;
import static com.wcm.site.servlets.RolloutWorkflowStarter.*;
import static com.wcm.site.util.AssetUtils.TRANSLATE_LANGUAGES_KEY_NAME;
import static com.wcm.site.util.AssetUtils.getTargetLanguageCopyResource;
import static com.wcm.site.util.CFMUtils.*;
import static com.wcm.site.util.LinkUtils.joinAsPath;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
import static com.wcm.site.workflow.brightcove.BrightcovePublishVideoUpdatesProcess.includeSubassetsToPathsList;
@Component(immediate = true, service = WorkflowProcess.class, property = {
WORKFLOW_PROCESS_LABEL + "=PS Content Fragment Smart Update Variation Language Copy" })
public class ContentFragmentSmartUpdateVariationLanguageCopyProcess extends WorkflowProcessBase implements WorkflowProcess {
private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentSmartUpdateVariationLanguageCopyProcess.class);
private static final String VARIATION_KEY_NAME = "variation";
@Reference
private WorkflowHelper workflowHelper;
@Reference
private PageManagerFactory pageManagerFactory;
@Reference
private LiveRelationshipManager liveRelationshipManager;
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) {
try {
ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession);
String sourcePath = getTargetPath(workItem, true);
WorkflowData data = workItem.getWorkflowData();
MetaDataMap metaDataMap = data.getMetaDataMap();
boolean skipRollout = getPersistedData(workItem, SKIP_ROLLOUT_ARG_NAME, false);
String selectedRegions = getPersistedData(workItem, SELECTED_REGIONS_ARG_NAME, StringUtils.EMPTY);
String updateLanguages = StringUtils.isNotEmpty(selectedRegions) ? selectedRegions : metaDataMap.get(TRANSLATE_LANGUAGES_KEY_NAME, StringUtils.EMPTY);
String variationName = StringUtils.isNotEmpty(selectedRegions) ? MASTER_VARIATION_NAME : metaDataMap.get(VARIATION_KEY_NAME, StringUtils.EMPTY);
if (StringUtils.isAnyEmpty(updateLanguages, variationName) || skipRollout) {
LOG.error("Update languages list or variation is empty or skipRollout is true, exiting");
persistReplicationList(workItem, workflowSession, resourceResolver, sourcePath, selectedRegions);
return;
}
String[] updateLanguagesArr = StringUtils.split(updateLanguages, ";");
Arrays.stream(updateLanguagesArr).forEach(language ->
updateVariation(resourceResolver, sourcePath, variationName, language));
if (resourceResolver.hasChanges()) {
resourceResolver.commit();
}
persistReplicationList(workItem, workflowSession, resourceResolver, sourcePath, selectedRegions);
} catch (IOException | RuntimeException e) {
LOG.error("Exception", e);
}
}
public void persistReplicationList(WorkItem workItem, WorkflowSession workflowSession, ResourceResolver resourceResolver, String sourcePath, String selectedRegions) {
String[] selectedRegionsArr = StringUtils.split(selectedRegions, ";");
PathContext context = new PathContext(sourcePath);
List<string> replicationPathList = new ArrayList<>(Arrays.asList(ArrayUtils.add(Arrays.stream(selectedRegionsArr)
.map(context::getPathWithLocale)
.map(LinkUtils::appendJcrContentIfMissing)
.toArray(String[]::new), LinkUtils.appendJcrContentIfMissing(sourcePath))));
includeSubassetsToPathsList(replicationPathList, resourceResolver, sourcePath, false);
for(String regionLocale : selectedRegionsArr) {
includeSubassetsToPathsList(replicationPathList, resourceResolver, context.getPathWithLocale(regionLocale), false);
}
persistData(workItem, workflowSession, PAGE_PATHS_KEY_NAME, replicationPathList.toArray());
}
private void updateVariation(ResourceResolver resourceResolver, String sourcePath, String variationName, String language) {
PathContext context = new PathContext(sourcePath);
try {
Resource targetLanguageCopy = getTargetLanguageCopyResource(sourcePath, language, resourceResolver);
Optional.ofNullable(targetLanguageCopy)
.map(resource -> resource.adaptTo(ContentFragment.class))
.ifPresent(CFMUtils::createVersion);
if (targetLanguageCopy == null) {
createCFLanguageCopy(resourceResolver, sourcePath, language);
}
Resource destinationDataFolder = resourceResolver.getResource(
StringUtils.removeEnd(getCFVariationPath(context.getPathWithLocale(language), StringUtils.EMPTY), "/"));
Resource destinationModelFolder = ResourceUtil.getOrCreateResource(resourceResolver, StringUtils.removeEnd(getCFModelVariationPath(context.getPathWithLocale(language), StringUtils.EMPTY),"/"), NT_UNSTRUCTURED, NT_UNSTRUCTURED, true);
Set<string> translatableFields = GLRepo.CONTENT_FRAGMENT.getTranslatableFields(resourceResolver);
createCFLanguageCopyVariationIfEmpty(resourceResolver, sourcePath, variationName, destinationDataFolder, destinationModelFolder);
Resource sourceVariationResource = resourceResolver.getResource(getCFVariationPath(sourcePath, variationName));
Resource destinationVariationResource = resourceResolver.getResource(
getCFVariationPath(context.getPathWithLocale(language), variationName));
syncElementsFromSource(resourceResolver, translatableFields, sourceVariationResource, destinationVariationResource, language);
setLastModified(resourceResolver, targetLanguageCopy);
} catch (IOException | RepositoryException | WorkflowException | RuntimeException e) {
LOG.error("Exception", e);
}
}
private void setLastModified(ResourceResolver resourceResolver, Resource targetLanguageCopy) {
Optional.ofNullable(targetLanguageCopy)
.map(resource -> resource.getChild(JCR_CONTENT))
.map(resource -> resource.adaptTo(ModifiableValueMap.class))
.ifPresent(valueMap -> {
valueMap.put(JCR_LASTMODIFIED, Calendar.getInstance());
valueMap.put(JCR_LAST_MODIFIED_BY, resourceResolver.getUserID());
});
}
private void syncElementsFromSource(ResourceResolver resourceResolver, Set<string> translatableFields, Resource sourceVariationResource, Resource destinationVariationResource, String language) throws PersistenceException {
if (!ObjectUtils.allNotNull(sourceVariationResource, sourceVariationResource.getValueMap(), destinationVariationResource, destinationVariationResource.getValueMap())) {
return;
}
ValueMap sourceVariationVM = sourceVariationResource.getValueMap();
ValueMap destinationVariationVM = destinationVariationResource.adaptTo(ModifiableValueMap.class);
sourceVariationVM.keySet()
.stream()
.filter(key -> (language.startsWith(Locale.ENGLISH.getLanguage())) ||
(!translatableFields.contains(StringUtils.substringBefore(key, "@"))))
.forEach(key -> destinationVariationVM.put(key, sourceVariationResource.getValueMap().get(key))
);
if (resourceResolver.hasChanges()) {
resourceResolver.commit();
}
}
private void createCFLanguageCopyVariationIfEmpty(ResourceResolver resourceResolver, String sourcePath, String variationName, Resource destinationDataFolder, Resource destinationModelFolder) throws PersistenceException {
Resource variationDataResource = destinationDataFolder.getChild(variationName);
Resource variationModelResource = destinationModelFolder.getChild(variationName);
if (variationDataResource == null) {
resourceResolver.copy(getCFVariationPath(sourcePath, variationName), destinationDataFolder.getPath());
}
if (!MASTER_VARIATION_NAME.equalsIgnoreCase(variationName) && variationModelResource == null) {
resourceResolver.copy(getCFModelVariationPath(sourcePath, variationName), destinationModelFolder.getPath());
}
}
private void createCFLanguageCopy(ResourceResolver resourceResolver, String sourcePath, String language) throws RepositoryException, WorkflowException, PersistenceException {
List<string> createdCopies = DamLanguageUtil.createLanguageCopyWithAssetRelations(resourceResolver, this.pageManagerFactory, sourcePath, new String[]{language});
if ((createdCopies == null) || (createdCopies.isEmpty())) {
throw new WorkflowException("Error while creating language copy for assets: " + sourcePath + ".");
}
for (String cfPath : createdCopies) {
ResourceUtils.clearProperty(resourceResolver, joinAsPath(cfPath, JCR_CONTENT), PROXY_PAGE_REFERENCE);
}
if (resourceResolver.hasChanges()) {
resourceResolver.commit();
}
}
private String getCFModelVariationPath(String cfPath, String variation) {
return joinAsPath(cfPath, JCR_CONTENT, "model/variations", variation);
}
}</string></string></string></string>
Links Localization
The second process is com.wcm.site.workflow.ContentFragmentLinkLocalisationProcess that is being described at https://exadel.com/news/aem-content-fragments-links-localization-part-1/
Proxy Page Rollout
The following Workflow is responsible for Proxy Pages rollout
var ROLLOUT_WORKFLOW_PAGE_MODEL_ID = "/var/workflow/models/site/rollout-proxy-page-workflow";
This process has 2 steps: Rollout and Proxy Page Reference setup.
Rollout Workflow
com.wcm.site.workflow.RolloutWorkflow:
package com.wcm.site.workflow;
import com.adobe.acs.commons.util.WorkflowHelper;
import com.day.cq.i18n.I18n;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.WCMException;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.wcm.site.localization.Locale;
import com.wcm.site.localization.PathContext;
import com.wcm.site.services.RolloutService;
import com.wcm.site.services.WorkflowNotificationService;
import com.wcm.site.util.PathContextHelper;
import com.wcm.site.util.RolloutPredicateHelper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.i18n.ResourceBundleProvider;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.wcm.site.servlets.RolloutWorkflowStarter.*;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
@Component(immediate = true, service = WorkflowProcess.class, property = {
WORKFLOW_PROCESS_LABEL + "=Rollout Workflow Process"})
public class RolloutWorkflow extends CQWorkflowProcessBase implements WorkflowProcess {
public static final String SUCCESS_MESSAGE_ROLLOUT_I18N = "rollout_workflow_success";
public static final String SUCCESS_MESSAGE_ROLLOUT_NO_PUBLISH_I18N = "rollout_workflow_no_publish_success";
public static final String SUCCESS_MESSAGE_PUBLISH_ONLY_I18N = "rollout_workflow_publish_only_success";
private static final Logger LOG = LoggerFactory.getLogger(RolloutWorkflow.class);
@Reference
private LiveRelationshipManager relationshipManager;
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Reference
private RolloutService rolloutService;
@Reference
private WorkflowHelper workflowHelper;
@Reference
private WorkflowNotificationService workflowNotificationService;
@Reference(target = "(component.name=org.apache.sling.i18n.impl.JcrResourceBundleProvider)")
private ResourceBundleProvider i18nProvider;
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaDataMap) throws WorkflowException {
final String path = getTargetPath(workItem);
ResourceResolver resolver = getResourceResolver(workflowHelper, workflowSession);
I18n i18n = new I18n(i18nProvider.getResourceBundle(java.util.Locale.US));
try {
Resource source = workflowHelper.getPageOrAssetResource(resolver, path);
PageManager pageManager = resolver.adaptTo(PageManager.class);
PathContext context = new PathContext(path);
String[] locales = getLocales(workItem);
boolean skipRollout = getPersistedData(workItem, SKIP_ROLLOUT_ARG_NAME, false);
List<string> targets = getTargetPaths(path, context, locales);
if (skipRollout) {
targets = getTargetPaths(path, context, ArrayUtils.removeElement(locales, java.util.Locale.ENGLISH.getLanguage()));
handleSkipRolloutFlow(workItem, workflowSession, targets, resolver, i18n);
return;
}
List<string> rolledOutPaths = rolloutService.rollout(source, targets, true);
persistData(workItem, workflowSession, PAGE_PATHS_KEY_NAME, rolledOutPaths.stream()
.filter(RolloutPredicateHelper.getFilterOutLiveCopySourcesPredicate(resolver, relationshipManager))
.toArray(String[]::new));
workflowNotificationService.addWorkflowCompletionStatus(workItem, WorkflowNotificationService.CompletionStatus.SUCCESS);
workflowNotificationService.addWorkflowComment(workItem, getSuccessMessage(targets, i18n, workItem));
} catch (IllegalStateException | WCMException | WorkflowException e) {
LOG.error(e.getMessage(), e);
workflowNotificationService.addWorkflowComment(workItem, e.getMessage());
workflowNotificationService.addWorkflowCompletionStatus(workItem, WorkflowNotificationService.CompletionStatus.WARN);
}
}
@NotNull
public static List<string> getTargetPaths(String path, PathContext context, String[] locales) {
return Arrays.stream(locales)
.map(locale -> PathContextHelper.getPathWithLocale(context, locale, PathContext.Site.lookup(path)))
.collect(Collectors.toList());
}
private String getSuccessMessage(List<string> targets, I18n i18n, WorkItem workItem) {
boolean skipReplicationAndFlush = getPersistedData(workItem, SKIP_REPLICATION_AND_FLUSH_ARG_NAME, false);
String messageKey = SUCCESS_MESSAGE_ROLLOUT_I18N;
if (skipReplicationAndFlush) {
messageKey = SUCCESS_MESSAGE_ROLLOUT_NO_PUBLISH_I18N;
}
boolean skipRollout = getPersistedData(workItem, SKIP_ROLLOUT_ARG_NAME, false);
if (skipRollout) {
messageKey = SUCCESS_MESSAGE_PUBLISH_ONLY_I18N;
}
String paths = targets.stream().collect(Collectors.joining("rn"));
return i18n.get(messageKey, null, paths);
}
public static String[] getLocales(WorkItem workItem) {
String selectedRegions = CQWorkflowProcessBase.getPersistedData(workItem.getWorkflow(), SELECTED_REGIONS_ARG_NAME, StringUtils.EMPTY);
return getLocalesFromIsoRegions(selectedRegions);
}
@NotNull
public static String[] getLocalesFromIsoRegions(String selectedRegions) {
return Stream.of(selectedRegions.split(";"))
.map(locale -> Locale.lookupByIsoLocale(locale, null))
.filter(Objects::nonNull)
.map(Locale::name)
.toArray(String[]::new);
}
private void handleSkipRolloutFlow(WorkItem workItem, WorkflowSession workflowSession, List<string> targets,
ResourceResolver resolver, I18n i18n) throws WorkflowException {
persistData(workItem, workflowSession, PAGE_PATHS_KEY_NAME, targets.stream()
.filter(RolloutPredicateHelper.getFilterOutNotRolledOutPathsPredicate(resolver))
.filter(RolloutPredicateHelper.getFilterOutLiveCopySourcesPredicate(resolver, relationshipManager))
.toArray(String[]::new));
workflowNotificationService.addWorkflowCompletionStatus(workItem, WorkflowNotificationService.CompletionStatus.SUCCESS);
workflowNotificationService.addWorkflowComment(workItem, getSuccessMessage(targets, i18n, workItem));
}
}
</string></string></string></string></string>
Proxy Page Reference setter
This workflow process adds a new property to CF’s jcr:content node – “pageReference”, so that we know which proxy page it represents. com.wcm.site.workflow.SetProxyPageReferenceWorkflow:
package com.wcm.site.workflow;
import com.adobe.acs.commons.util.WorkflowHelper;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.wcm.site.util.CFMUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.wcm.site.services.impl.CFProxyReferenceUpdateConsumer.PROXY_PAGE_REFERENCE;
import static com.wcm.site.servlets.datasource.AbstractContentFragmentDataSourceServlet.PN_FRAGMENT_PATH;
import static com.wcm.site.util.LinkUtils.joinAsPath;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
import static org.apache.commons.lang3.ArrayUtils.nullToEmpty;
@Component(immediate = true, service = WorkflowProcess.class, property = {
WORKFLOW_PROCESS_LABEL + "=Set Proxy Page Reference Workflow Process"})
public class SetProxyPageReferenceWorkflow extends CQWorkflowProcessBase implements WorkflowProcess {
private static final Logger LOG = LoggerFactory.getLogger(SetProxyPageReferenceWorkflow.class);
@Reference
private WorkflowHelper workflowHelper;
@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaDataMap) throws WorkflowException {
final String targetPath = getTargetPath(workItem);
List<string> pagePathsList = new ArrayList<>(Arrays.asList(nullToEmpty(getPersistedData(workItem, PAGE_PATHS_KEY_NAME, new String[]{}))));
try {
ResourceResolver resolver = getResourceResolver(workflowHelper, workflowSession);
String fragmentPath = CFMUtils.getFragmentPathFromPagePropertiesForResource(resolver.getResource(targetPath));
if (StringUtils.isNotEmpty(fragmentPath) && resolver.getResource(fragmentPath) != null) {
processPage(resolver, targetPath);
}
pagePathsList.forEach(pagePath -> processPage(resolver, pagePath));
if (resolver.hasChanges()) {
resolver.commit();
}
} catch (PersistenceException ex) {
LOG.error(ex.getMessage(), ex);
}
}
private static void processPage(ResourceResolver resolver, String pagePath) {
String fragmentPath = resolver.getResource(joinAsPath(pagePath, JCR_CONTENT)).getValueMap().get(PN_FRAGMENT_PATH, String.class);
ModifiableValueMap cfValueMap = resolver.getResource(joinAsPath(fragmentPath, JCR_CONTENT)).adaptTo(ModifiableValueMap.class);
cfValueMap.put(PROXY_PAGE_REFERENCE, pagePath);
}
}
</string>
In the next article, we’ll take a look at the corresponding processes for content fragments and Pages replication in our flow. Want to you code to look nice and work well, use our AEM development services to your advantage!
Author: Iryna Ason
Was this article useful for you?
Get in the know with our publications, including the latest expert blogs
End-to-End Digital Transformation
Reach out to our experts to discuss how we can elevate your business