Content Fragments in Depth. CF Management Workflow. Part 3.3

Exadel Digital Experience Team Tech Tips May 24, 2023 7 min read

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";
See more See less

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>
See more See less

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";
See more See less

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>
See more See less

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>
See more See less

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