Tech Tips

Content Fragments in Depth. Content Fragment Management Workflow in AEM. Part 3.2

We’ve looked at content fragments and custom client libraries to map a steady AEM workflow. In this article we’ll continue to take a look at what’s happening at the back end. As a team who provides AEM development services, we’ll describe all the processes mentioned in part 3.2 about Content Fragment workflow in AEM. The Custom Client lib described in part 3.1 calls either a servlet “.rollout-workflow.json” or “.cf-controller.json”, which will trigger a corresponding workflow or service with paths and other data needed. These are:

  1. CF rollout to the selected locales
  2. CF replication
  3. Proxy Page creation and linking it with CF selected
  4. Proxy Page rollout
  5. Proxy Page replication

Workflow in AEM: Starter Servlet

This servlet is aimed at calling a corresponding workflow.

package com.wcm.site.servlets;
 
import com.adobe.granite.workflow.PayloadMap;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.HistoryItem;
import com.adobe.granite.workflow.exec.Status;
import com.adobe.granite.workflow.exec.Workflow;
import com.adobe.granite.workflow.exec.WorkflowData;
import com.adobe.granite.workflow.model.WorkflowModel;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import javax.servlet.Servlet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
 
import static com.day.cq.dam.api.DamConstants.NT_DAM_ASSET;
import static com.day.cq.wcm.api.NameConstants.NT_PAGE;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
 
@Component(service = Servlet.class, configurationPolicy = ConfigurationPolicy.REQUIRE)
@SlingServletResourceTypes(
        resourceTypes={NT_PAGE, NT_DAM_ASSET},
        extensions="json",
        selectors="rollout-workflow")
public class RolloutWorkflowStarter extends SlingSafeMethodsServlet {
    private static final String ACTION_PARAMETER_NAME = "action";
    private static final String MODEL_ID_NAME = "modelId";
    private static final String OUTPUT_MESSAGE_FIELD_NAME = "message";
    private static final String OUTPUT_STATUS_FIELD_NAME = "status";
    private static final String WORKFLOW_ID_FIELD_NAME = "workflowId";
    private static final String WORKFLOW_IDS_FIELD_NAME = "workflowIds";
    private static final String WORKFLOW_GENERAL_ERROR_MSG = "Failed to start workflow";
    public static final String SELECTED_REGIONS_ARG_NAME = "regions";
 
    public static final String SKIP_REPLICATION_AND_FLUSH_ARG_NAME = "skipReplicationAndFlush";
    public static final String BATCH_CDN_FLUSH_FLAG_ARG_NAME = "batchCDNFlushFlag";
    public static final String SKIP_ROLLOUT_ARG_NAME = "skipRollout";
 
 
    private static final Logger LOG = LoggerFactory.getLogger(RolloutWorkflowStarter.class);
 
 
    @Override
    public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        response.setContentType(APPLICATION_JSON);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
 
        JsonObject jsonResponse = new JsonObject();
        jsonResponse.addProperty(OUTPUT_MESSAGE_FIELD_NAME, WORKFLOW_GENERAL_ERROR_MSG);
        String action = StringUtils.trimToEmpty(request.getParameter(ACTION_PARAMETER_NAME));
 
        if (StringUtils.isNotBlank(action)) {
            Operation operation = Operation.lookup(action);
            if (operation != null) {
                jsonResponse = operation.action.processRequest(request);
            }
        }
        response.getWriter().println(jsonResponse.toString());
    }
 
   private static class Actions {
        static Action start() {
            return slingRequest -> {
                JsonObject jsonObject = new JsonObject();
                try {
 
                    String modelId = StringUtils.trimToEmpty(slingRequest.getParameter(MODEL_ID_NAME));
                    WorkflowSession workflowSession = slingRequest.getResourceResolver().adaptTo(WorkflowSession.class);
 
                    String selectedRegions = StringUtils.trimToEmpty(slingRequest.getParameter(SELECTED_REGIONS_ARG_NAME));
                    boolean skipReplicationAndFlush = BooleanUtils.toBoolean(StringUtils.trimToEmpty(slingRequest.getParameter(SKIP_REPLICATION_AND_FLUSH_ARG_NAME)));
                    boolean skipRollout = BooleanUtils.toBoolean(StringUtils.trimToEmpty(slingRequest.getParameter(SKIP_ROLLOUT_ARG_NAME)));
                    String[] workflowIds = StringUtils.split(StringUtils.trimToEmpty(slingRequest.getParameter(WORKFLOW_IDS_FIELD_NAME)), ",");
                    String[] pagePaths = preparePagePaths(workflowSession, workflowIds);
 
                    WorkflowModel workflowModel = workflowSession.getModel(modelId);
                    WorkflowData workflowData = workflowSession.newWorkflowData(PayloadMap.TYPE_JCR_PATH, slingRequest.getResource().getPath());
 
                    if(pagePaths.length > 0) {
                        workflowData.getMetaDataMap().put("pagePaths", pagePaths);
                    }
 
                    workflowData.getMetaDataMap().put(SELECTED_REGIONS_ARG_NAME, selectedRegions);
                    workflowData.getMetaDataMap().put(SKIP_REPLICATION_AND_FLUSH_ARG_NAME, skipReplicationAndFlush);
                    workflowData.getMetaDataMap().put(SKIP_ROLLOUT_ARG_NAME, skipRollout);
                    workflowData.getMetaDataMap().put(BATCH_CDN_FLUSH_FLAG_ARG_NAME, true);
 
                    Workflow workflow = workflowSession.startWorkflow(workflowModel, workflowData, Collections.emptyMap());
                    jsonObject.addProperty(WORKFLOW_ID_FIELD_NAME, workflow.getId());
 
                } catch (WorkflowException e) {
                    LOG.error("WorkflowException", e);
                    jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, WORKFLOW_GENERAL_ERROR_MSG);
                }
 
                return jsonObject;
            };
        }
       static Action status() {
           return slingRequest -> {
               JsonObject jsonObject = new JsonObject();
               try {
 
                   String workflowId = StringUtils.trimToEmpty(slingRequest.getParameter(WORKFLOW_ID_FIELD_NAME));
                   if (StringUtils.isEmpty(workflowId)) {
                       LOG.error("Workflow Id can't be empty");
                       jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, "Workflow id can't be empty, exiting");
                       return jsonObject;
                   }
 
                   WorkflowSession workflowSession = slingRequest.getResourceResolver().adaptTo(WorkflowSession.class);
                   Workflow workflowModel = workflowSession.getWorkflow(workflowId);
 
                   if (workflowModel.isActive()) {
                       jsonObject.addProperty(OUTPUT_STATUS_FIELD_NAME, Status.ACTIVE.name());
                       return jsonObject;
                   }
 
                   jsonObject.addProperty(OUTPUT_STATUS_FIELD_NAME, workflowModel.getState());
 
                   List<historyitem> workflowHistory = workflowSession.getHistory(workflowModel);
                   if (!workflowHistory.isEmpty()) {
                       HistoryItem lastItem = workflowHistory.get(workflowHistory.size() - 1);
                       String comment = lastItem.getComment();
                       String status = lastItem.getWorkItem().getWorkflow().getWorkflowData().getMetaDataMap().get("completionStatus", StringUtils.EMPTY);
 
                       jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, comment);
 
                       if ("SUCCESS".equals(status)) {
                           jsonObject.addProperty(OUTPUT_STATUS_FIELD_NAME, status);
                       }
                   }
 
 
               } catch (WorkflowException e) {
                   LOG.error("WorkflowException", e);
                   jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, WORKFLOW_GENERAL_ERROR_MSG);
               }
 
               return jsonObject;
           };
       }
       static String[] preparePagePaths(WorkflowSession workflowSession, String ... workflowIds) {
           return Stream.of(workflowIds)
                    .map(workflowId -> getPagePathsProp(workflowSession, workflowId))
                    .flatMap(Stream::of)
                    .toArray(String[]::new);
       }
 
       static String[] getPagePathsProp(WorkflowSession workflowSession, String workflowId) {
            try {
                Workflow workflow = workflowSession.getWorkflow(workflowId);
                List<historyitem> workflowHistory = workflowSession.getHistory(workflow);
                HistoryItem lastItem = workflowHistory.get(workflowHistory.size() - 1);
                return lastItem.getWorkItem().getWorkflowData().getMetaDataMap().get("pagePaths", new String[0]);
            } catch (WorkflowException ex) {
                LOG.error("Exception on getting pagePaths", ex);
                return new String[0];
            }
       }
    }
 
    private enum Operation {
        START(Actions.start()),
        STATUS(Actions.status());
 
        private Action action;
 
        Operation(Action action) {
            this.action = action;
        }
 
        public static Operation lookup(String type) {
            return Arrays.stream(values())
                    .filter(operation -> operation.name().equalsIgnoreCase(type))
                    .findFirst().orElse(null);
        }
    }
 
    @FunctionalInterface
    private interface Action {
        JsonObject processRequest(SlingHttpServletRequest request);
    }
 
}
 
</historyitem></historyitem>
See more See less

Content Fragment Service

The other servlet used in our JS clientlib does not trigger any workflows in AEM, but performs the action itself.

package com.wcm.site.servlets;
 
 
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.granite.workflow.exec.Workflow;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.WCMException;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.wcm.site.localization.PathContext;
import com.wcm.site.models.webrefresh.cf.CFProxyPageSupport;
import com.wcm.site.services.CFProxyPageService;
import com.wcm.site.services.ReplicationService;
import com.wcm.site.util.CFMUtils;
import com.wcm.site.util.LinkUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import javax.servlet.Servlet;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
 
import static com.adobe.cq.xf.ExperienceFragmentsConstants.PN_FRAGMENT_PATH;
import static com.day.cq.commons.jcr.JcrConstants.JCR_CONTENT;
import static com.day.cq.wcm.api.NameConstants.NT_PAGE;
import static com.wcm.site.services.impl.CFProxyPageServiceImpl.getSearchPaths;
import static com.wcm.site.servlets.RolloutWorkflowStarter.SELECTED_REGIONS_ARG_NAME;
import static com.wcm.site.servlets.datasource.CFSecondaryOptionsDataSourceServlet.DIALOG_FIELDS_PATH_PATTERN;
import static com.wcm.site.util.PathContextHelper.isContentFragment;
import static com.wcm.site.util.ResourceUtils.deleteAndCommit;
import static com.wcm.site.util.ServiceUtils.GSON;
import static com.wcm.site.workflow.RolloutWorkflow.getLocalesFromIsoRegions;
import static com.wcm.site.workflow.RolloutWorkflow.getTargetPaths;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
@Component(service = Servlet.class, configurationPolicy = ConfigurationPolicy.REQUIRE)
@SlingServletResourceTypes(
        resourceTypes= {DamConstants.NT_DAM_ASSET, NT_PAGE},
        extensions="json",
        selectors="cf-controller")
public class RolloutCFControllerServlet extends SlingSafeMethodsServlet {
    private static final String ACTION_PARAMETER_NAME = "action";
 
    private static final String OUTPUT_MESSAGE_FIELD_NAME = "message";
    private static final String OUTPUT_STATUS_FIELD_NAME = "status";
    private static final String CONTENT_FRAGMENT_SUB_TYPE = "content-fragment";
 
    private static final String CF_REFERENCES_FN = "cfReferences";
    private static final String INVALID_CF_REFERENCES_FN = "invalidCfReferences";
    private static final String WORKFLOW_GENERAL_ERROR_MSG = "Failed to start workflow";
    private static final String PAGE_CREATE_FAILED_ERROR_MSG = "Failed to create a proxy page";
 
 
    private static final Logger LOG = LoggerFactory.getLogger(RolloutCFControllerServlet.class);
 
    @Reference
    private transient CFProxyPageService cfProxyPageService;
 
    @Reference
    private transient ReplicationService replicationService;
 
    @Override
    public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        response.setContentType(APPLICATION_JSON);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
 
        JsonObject jsonResponse = new JsonObject();
        jsonResponse.addProperty(OUTPUT_MESSAGE_FIELD_NAME, WORKFLOW_GENERAL_ERROR_MSG);
        String action = StringUtils.trimToEmpty(request.getParameter(ACTION_PARAMETER_NAME));
 
        if (StringUtils.isNotBlank(action)) {
            Operation operation = Operation.lookup(action);
            if (operation != null) {
                jsonResponse = operation.action.processRequest(request, cfProxyPageService, replicationService);
            }
        }
        response.getWriter().println(jsonResponse.toString());
    }
 
   private static class Actions {
        static Action getCfReferences() {
            return (slingRequest, proxyPageService, replicationService) -> {
                JsonObject jsonObject = new JsonObject();
 
                List<string> cfRefs = getCFReferences(slingRequest, true);
 
                JsonArray jsonArray = GSON.toJsonTree(cfRefs).getAsJsonArray();
                jsonObject.add(CF_REFERENCES_FN, jsonArray);
 
                return jsonObject;
            };
        }
        static Action doCfPostValidation() {
           return (slingRequest, proxyPageService, replicationService) -> {
               JsonObject jsonObject = new JsonObject();
               String selectedRegions = StringUtils.trimToEmpty(slingRequest.getParameter(SELECTED_REGIONS_ARG_NAME));
               String[] selectedRegionsArr = ArrayUtils.add(StringUtils.split(selectedRegions, ";"), Locale.ENGLISH.getLanguage());
 
               List<string> references = new ArrayList<>(RolloutCFControllerServlet.getCFReferences(slingRequest, false));
               references.add(slingRequest.getResource().getPath());
 
               List<string> nonExistingTargets = references.stream()
                       .map(PathContext::new)
                       .map(pathContext -> Arrays.stream(selectedRegionsArr)
                               .map(pathContext::getPathWithLocale)
                               .toArray(String[]::new))
                       .flatMap(Arrays::stream)
                       .filter(path -> slingRequest.getResourceResolver().getResource(path) == null)
                       .collect(Collectors.toList());
 
 
               JsonArray jsonArray = GSON.toJsonTree(nonExistingTargets).getAsJsonArray();
               jsonObject.add(INVALID_CF_REFERENCES_FN, jsonArray);
 
               return jsonObject;
           };
       }
        static Action getProxyPage() {
           return (slingRequest, proxyPageService, replicationService) -> {
               JsonObject jsonObject = new JsonObject();
 
               String cfPath = slingRequest.getResource().getPath();
               CFProxyPageSupport proxyPageSupportModel = proxyPageService.getCfProxyPageSupportModel(cfPath, slingRequest.getResourceResolver());
 
               String proxyPagePath = StringUtils.EMPTY;
               boolean intermediatePageRequired = false;
 
               if (proxyPageSupportModel != null) {
                   List<string> searchPaths = getSearchPaths(cfPath, proxyPageSupportModel);
 
                   if (Operation.CREATE_PROXY_PAGE.equals(Operation.lookup(StringUtils.trimToEmpty(slingRequest.getParameter(ACTION_PARAMETER_NAME))))
                        && proxyPageService.isCustomRolloutWFConflictResolutionReplaceEnabled()) {
                       searchPaths = Collections.singletonList(proxyPageSupportModel.getRootFolderPath());
                   }
 
                   proxyPagePath = proxyPageService
                           .findProxyPageResources(slingRequest.getResourceResolver(), cfPath, searchPaths)
                           .stream()
                           .map(resource -> LinkUtils.removeJcrContentIfExists(resource.getPath()))
                           .findFirst()
                           .orElse(StringUtils.EMPTY);
                   intermediatePageRequired = proxyPageSupportModel.isIntermediatePageRequired();
               }
 
               jsonObject.addProperty("path", proxyPagePath);
               jsonObject.addProperty("intermediatePageRequired", intermediatePageRequired);
 
               return jsonObject;
           };
       }
       static Action createProxyPage() {
           return (slingRequest, proxyPageService, replicationService) -> {
               JsonObject jsonObject = new JsonObject();
               JsonObject proxyPageObject = getProxyPage().processRequest(slingRequest, proxyPageService, replicationService);
               if (proxyPageObject != null && proxyPageObject.has("path") && StringUtils.isNotEmpty(proxyPageObject.get("path").getAsString())) {
                   return proxyPageObject;
               }
               String cfPath = slingRequest.getResource().getPath();
               CFProxyPageSupport proxyPageSupportModel = proxyPageService.getCfProxyPageSupportModel(cfPath, slingRequest.getResourceResolver());
 
               String proxyPagePath = StringUtils.EMPTY;
               boolean intermediatePageRequired = false;
 
               try {
                   if (proxyPageSupportModel != null) {
                       Page finalPage = proxyPageService.createProxyPage(cfPath, slingRequest.getResourceResolver());
                       intermediatePageRequired = proxyPageSupportModel.isIntermediatePageRequired();
                       if (finalPage != null) {
                           proxyPagePath = finalPage.getPath();
                       }
 
                   }
                   jsonObject.addProperty("path", proxyPagePath);
                   jsonObject.addProperty("intermediatePageRequired", intermediatePageRequired);
                   return jsonObject;
 
               } catch (IllegalStateException e) {
                   LOG.error("IllegalStateException", e);
                   jsonObject.addProperty(OUTPUT_STATUS_FIELD_NAME, Workflow.State.ABORTED.name());
                   jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, e.getMessage());
               } catch (RuntimeException | IOException | WCMException e) {
                   LOG.error("Exception", e);
                   jsonObject.addProperty(OUTPUT_STATUS_FIELD_NAME, Workflow.State.ABORTED.name());
                   jsonObject.addProperty(OUTPUT_MESSAGE_FIELD_NAME, PAGE_CREATE_FAILED_ERROR_MSG);
               }
 
               return jsonObject;
           };
       }
 
    }
 
    @NotNull
    public static List<string> getCFReferences(SlingHttpServletRequest slingRequest, boolean filterOutInvalid) {
        Resource resource = slingRequest.getResource();
        ContentFragment cf = resource.adaptTo(ContentFragment.class);
        String modelPath = CFMUtils.getTemplatePath(cf);
        ResourceResolver resourceResolver = slingRequest.getResourceResolver();
 
        String pathToCFModelElements = String.format(DIALOG_FIELDS_PATH_PATTERN,
                modelPath, JCR_CONTENT);
        Resource cfModelElementRoot = resourceResolver.getResource(pathToCFModelElements);
 
        if (cfModelElementRoot == null) {
            return Collections.emptyList();
        }
 
        List<string> cfElements = IterableUtils.toList(cfModelElementRoot.getChildren()).stream()
                .map(Resource::getValueMap)
                .filter(valueMap -> StringUtils.equalsIgnoreCase(CONTENT_FRAGMENT_SUB_TYPE, valueMap.get("subType", String.class)))
                .map(valueMap -> valueMap.get("name", StringUtils.EMPTY))
                .collect(Collectors.toList());
 
        return cfElements.stream()
                .map(elementName -> CFMUtils.getValue(Optional.ofNullable(cf), elementName, String[].class))
                .filter(Objects::nonNull)
                .flatMap(Arrays::stream)
                .filter(StringUtils::isNotBlank)
                .filter(ref -> !filterOutInvalid || (filterOutInvalid && resourceResolver.getResource(ref) != null))
                .collect(Collectors.toList());
    }
 
    private enum Operation {
        CF_REFERENCES(Actions.getCfReferences()),
        CF_POST_VALIDATION(Actions.doCfPostValidation()),
        GET_PROXY_PAGE(Actions.getProxyPage()),
        CREATE_PROXY_PAGE(Actions.createProxyPage());
 
        private Action action;
 
        Operation(Action action) {
            this.action = action;
        }
 
        public static Operation lookup(String type) {
            return Arrays.stream(values())
                    .filter(operation -> operation.name().equalsIgnoreCase(type))
                    .findFirst().orElse(null);
        }
    }
 
    @FunctionalInterface
    private interface Action {
        JsonObject processRequest(SlingHttpServletRequest request, CFProxyPageService cfProxyPageService, ReplicationService replicationService);
    }
 
}
 
</string></string></string></string></string></string>
See more See less

In the next article we’ll look closely at those workflows in AEM that are being triggered. Stay tuned!

Author: Iryna Ason