Content Fragments in Depth. CF Management Workflow. Part 3.3Content Fragments in Depth. CF Management Workflow. Part 3.3

Tech Tips

7 min read

DX 20

Tags

#AEM

#Content Fragments

#Digital marketing Technology

Share

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

Resource Hub

Our Latest Stories & Industry Insights

View Resource Hub

Dark geometric prism with glowing teal outlines and abstract digital particles on a dark background.

Content Fragments in Depth. CF Management Workflow. Part 3.3

7 min read

May 24, 2023

#AEM #Content Fragments #Digital Experience #Digital Marketing Technology

Blue wireless computer mouse next to a laptop keyboard with pink backlit keys on dark surface.

Content Fragments in Depth. AEM Workflow Management. Part 3.1

7 min read

May 10, 2023

#AEM #Content Fragments #Digital Experience #Digital Marketing Technology

3D blue cubes connected by glowing pink lines on a dark surface representing a digital network.

AEM Content Fragments in Depth. Custom Schemas. Part 2

10 min read

May 3, 2023

#AEM #Content Fragments #Digital Experience #Digital Marketing Technology

Empty picture frame with red and green edges on teal textured wall.

How to Add Image Management to an AEM TouchUI Dialog: Tutorial

16 min read

October 12, 2021

#AEM #Open Source #Touch UI #Digital Marketing Technology #EXADEL Authoring Kit For AEM

Two people sitting at a table with a laptop.

Let’s make your next project faster, safer, smarter.

Get In Touch