AEM Content Fragments: Links Localization. Part 2

Exadel Digital Experience Team Tech Insights November 30, 2021 8 min read

In the previous article, we took a look at how to set up a translation project and correctly configure links rewriting during New Lang copy creation. Now we’re going to look at more cases you might encounter during links localization and how to approach a variety of situations.

Update All AEM Content Fragment Variations (Language Copy)

Before we can update a language copy (all variations), we have to:

  • Select your CF
  • Click References → Language copies
  • Check locales to update
  • Select Add to existing translation project
  • Then select our Site-com-translation-project created before
  • Select Update All variations
  • Click Start

AEM’s ‘’DAM Update Language Copy” workflow is being triggered. We’ll have to introduce a new step for this WF to localize the links correctly.

ContentFragmentUpdateLanguageCopyProcess.java:

package com.wcm.site.workflow;
 
import com.adobe.acs.commons.util.WorkflowHelper;
import com.adobe.acs.commons.util.visitors.TraversalException;
import com.adobe.acs.commons.util.visitors.TreeFilteringResourceVisitor;
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.util.AssetUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
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.Session;
import java.util.Arrays;
 
import static com.wcm.site.util.CFMUtils.isContentFragment;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
 
@Component(immediate = true, service = WorkflowProcess.class, property = {
        WORKFLOW_PROCESS_LABEL + "=Site.com Content Fragment Update Language Copy" })
public class ContentFragmentUpdateLanguageCopyProcess extends WorkflowProcessBase implements WorkflowProcess {
    private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentUpdateLanguageCopyProcess.class);
    private static final String TRANSLATE_LANGUAGES_KEY_NAME = "translateLanguages";
 
    @Reference
    private WorkflowHelper workflowHelper;
 
    @Reference
    private PageManagerFactory pageManagerFactory;
 
    @Reference
    private LiveRelationshipManager liveRelationshipManager;
 
    @Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) throws WorkflowException {
        ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession);
        String sourcePath = getTargetPath(workItem, true);
        Resource source = resourceResolver.getResource(sourcePath);
        WorkflowData data = workItem.getWorkflowData();
        MetaDataMap metaDataMap = data.getMetaDataMap();
 
        if (AssetUtils.isFolder(source)) {
            processFolder(workItem, workflowSession, source, metaDataMap);
            workflowSession.terminateWorkflow(workItem.getWorkflow());
            return;
        }
 
        if (source == null || !isContentFragment(source)) {
            LOG.error("Resource not found or not content fragment {}", sourcePath);
            return;
        }
 
        String updateLanguages = metaDataMap.get(TRANSLATE_LANGUAGES_KEY_NAME, StringUtils.EMPTY);
        if (StringUtils.isEmpty(updateLanguages)) {
            LOG.error("Update languages list is empty, exiting");
            return;
        }
        String[] updateLanguagesArr = StringUtils.split(updateLanguages, ";");
 
        Arrays.stream(updateLanguagesArr).forEach(language ->
            DamLanguageUtil.replaceUpdatedAsset(sourcePath,
                    getTargetLanguageCopyPath(sourcePath, language, resourceResolver, null),
                    resourceResolver.adaptTo(Session.class),
                    pageManagerFactory,
                    resourceResolver
 
            )
        );
    }
 
 
    private void processFolder(WorkItem workItem, WorkflowSession workflowSession, Resource source, MetaDataMap metaData) {
        try {
            TreeFilteringResourceVisitor cfVisitor = new TreeFilteringResourceVisitor();
            cfVisitor.setTraversalFilter(resource -> cfVisitor.isFolder(resource) || isContentFragment(resource));
            cfVisitor.setResourceVisitor((resource, level) -> {
                if (isContentFragment(resource)) {
                    startWorkflow(workflowSession, workItem.getWorkflow().getWorkflowModel().getId(), resource.getPath(), metaData);
                }
            });
 
            cfVisitor.accept(source);
        } catch (TraversalException e) {
            LOG.error("Exception", e);
        }
    }
 
 
    public static Resource getTargetLanguageCopyResource(String sourcePath, String language, ResourceResolver resolver) {
        return Optional.ofNullable(DamLanguageUtil.getLanguageCopy(sourcePath, language, resolver))
                .map(asset -> asset.adaptTo(Resource.class))
                .orElse(null);
    }
 
    public static String getTargetLanguageCopyPath(String sourcePath, String language, ResourceResolver resolver, String fallback) {
        if (isFolder(resolver.getResource(sourcePath))) {
            PathContext context = new PathContext(sourcePath);
            String targetPath = StringUtils.removeEnd(context.getPathWithLocale(language), "/");
            return resolver.getResource(targetPath) != null ? targetPath : fallback;
        }
        return Optional.ofNullable(getTargetLanguageCopyResource(sourcePath, language, resolver))
                .map(Resource::getPath)
                .orElse(fallback);
    }
 
 
}
See more See less

And the workflow itself /var/workflow/models/dam/dam-update-language-copy.xml:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="cq:WorkflowModel"
    sling:resourceType="cq/workflow/components/model"
    description="This workflow updates language copies for assets"
    title="DAM Update Language Copy">
    <metaData
        cq:generatingPage="/conf/global/settings/workflow/models/dam/dam-update-language-copy/jcr:content"
        cq:lastModified="{Long}1610816334147"
        cq:lastModifiedBy="admin"
        jcr:primaryType="nt:unstructured"/>
    <nodes jcr:primaryType="nt:unstructured">
        <node0
            jcr:primaryType="cq:WorkflowNode"
            title="Start"
            type="START">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node0>
        <node1
            jcr:primaryType="cq:WorkflowNode"
            description="Updates language copy of assets"
            title="Update Language Copy"
            type="PROCESS">
            <metaData
                jcr:primaryType="nt:unstructured"
                PROCESS="com.day.cq.dam.core.impl.process.UpdateAssetLanguageCopyProcess"
                PROCESS_AUTO_ADVANCE="true"/>
        </node1>
        <node2
            jcr:primaryType="cq:WorkflowNode"
            title="Site.com Content Fragment Update Language Copy"
            type="PROCESS">
            <metaData
                jcr:primaryType="nt:unstructured"
                PROCESS="com.wcm.site.workflow.ContentFragmentUpdateLanguageCopyProcess"
                PROCESS_AUTO_ADVANCE="true"/>
        </node2>
        <node3
            jcr:primaryType="cq:WorkflowNode"
            title="Content Fragment Localisation WFs Starter"
            type="PROCESS">
            <metaData
                jcr:primaryType="nt:unstructured"
                PROCESS="com.wcm.site.workflow.ContentFragmentLocalisationWFStarterProcess"
                PROCESS_ARGS="modelId:/var/workflow/models/site/cf-localisation-workflow"
                PROCESS_AUTO_ADVANCE="true"/>
        </node3>
        <node4
            jcr:primaryType="cq:WorkflowNode"
            title="End"
            type="END">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node4>
    </nodes>
    <transitions jcr:primaryType="nt:unstructured">
        <node0_x0023_node1
            jcr:primaryType="cq:WorkflowTransition"
            from="node0"
            rule=""
            to="node1">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node0_x0023_node1>
        <node1_x0023_node2
            jcr:primaryType="cq:WorkflowTransition"
            from="node1"
            rule=""
            to="node2">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node1_x0023_node2>
        <node2_x0023_node3
            jcr:primaryType="cq:WorkflowTransition"
            from="node2"
            rule=""
            to="node3">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node2_x0023_node3>
        <node3_x0023_node4
            jcr:primaryType="cq:WorkflowTransition"
            from="node3"
            rule=""
            to="node4">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node3_x0023_node4>
    </transitions>
</jcr:root>
See more See less

Update AEM Content Fragment Single Variation

If we want to update one particular content fragment variation rather than all, a different AEM WF is triggered, so we have to make some adjustments.

ContentFragmentUpdateVariationLanguageCopyProcess.java

package com.wcm.site.workflow;
 
import com.adobe.acs.commons.util.WorkflowHelper;
import com.adobe.cq.dam.cfm.ContentFragment;
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.wcm.api.PageManagerFactory;
import com.day.cq.wcm.msm.api.LiveRelationshipManager;
import com.wcm.site.localization.PathContext;
import com.wcm.site.util.CFMUtils;
import com.wcm.site.util.ResourceUtils;
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 java.util.Arrays;
import java.util.Calendar;
import java.util.Optional;
 
import static com.day.cq.commons.jcr.JcrConstants.*;
import static com.wcm.site.util.CFMUtils.getCFVariationPath;
import static com.wcm.site.util.CFMUtils.isContentFragment;
import static com.wcm.site.util.LinkUtils.joinAsPath;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
 
@Component(immediate = true, service = WorkflowProcess.class, property = {
        WORKFLOW_PROCESS_LABEL + "=Site.com Content Fragment Update Variation Language Copy" })
public class ContentFragmentUpdateVariationLanguageCopyProcess extends WorkflowProcessBase implements WorkflowProcess {
    private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentUpdateVariationLanguageCopyProcess.class);
    private static final String VARIATION_KEY_NAME = "variation";
    private static final String MASTER_VARIATION_NAME = "master";
 
 
    @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);
            Resource source = resourceResolver.getResource(sourcePath);
 
            if (source == null || !isContentFragment(source)) {
                LOG.error("Resource not found or not content fragment {}", sourcePath);
                return;
            }
 
            WorkflowData data = workItem.getWorkflowData();
            MetaDataMap metaDataMap = data.getMetaDataMap();
 
            String updateLanguages = metaDataMap.get(TRANSLATE_LANGUAGES_KEY_NAME, StringUtils.EMPTY);
            String variationName = metaDataMap.get(VARIATION_KEY_NAME, StringUtils.EMPTY);
            if (StringUtils.isAnyEmpty(updateLanguages, variationName)) {
                LOG.error("Update languages list or variation is empty, exiting");
                return;
            }
            String[] updateLanguagesArr = StringUtils.split(updateLanguages, ";");
 
            Arrays.stream(updateLanguagesArr).forEach(language ->
                        copyOrReplaceVariation(resourceResolver, sourcePath, variationName, language));
 
            if (resourceResolver.hasChanges()) {
                resourceResolver.commit();
            }
        } catch (Exception e) {
            LOG.error("Exception", e);
        }
 
    }
 
    private void copyOrReplaceVariation(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);
 
            Resource destinationData = resourceResolver.getResource(StringUtils.removeEnd(getCFVariationPath(context.getPathWithLocale(language), StringUtils.EMPTY), "/"));
            Resource destinationModel = ResourceUtil.getOrCreateResource(resourceResolver, StringUtils.removeEnd(getCFModelVariationPath(context.getPathWithLocale(language), StringUtils.EMPTY), "/"), NT_UNSTRUCTURED, NT_UNSTRUCTURED, true);
            if (!ObjectUtils.allNotNull(destinationData, destinationModel)) {
                LOG.error("destination is null");
                return;
            }
 
            Resource variationDataResource = destinationData.getChild(variationName);
            Resource variationModelResource = destinationModel.getChild(variationName);
 
            ResourceUtils.delete(resourceResolver, variationDataResource);
            ResourceUtils.delete(resourceResolver, variationModelResource);
 
            if (resourceResolver.hasChanges()) {
                resourceResolver.commit();
            }
 
            resourceResolver.copy(getCFVariationPath(sourcePath, variationName), destinationData.getPath());
            if (!MASTER_VARIATION_NAME.equalsIgnoreCase(variationName)) {
                resourceResolver.copy(getCFModelVariationPath(sourcePath, variationName), destinationModel.getPath());
            }
 
            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());
                    });
 
        } catch (PersistenceException e) {
            LOG.error("Exception", e);
        }
    }
 
    private String getCFModelVariationPath(String cfPath, String variation) {
        return joinAsPath(cfPath, JCR_CONTENT, "model/variations", variation);
    }
}
See more See less

And a workflow config to change /var/workflow/models/dam/dam-update-variation-language-copy.xml:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:isCheckedOut="{Boolean}false"
    jcr:primaryType="cq:WorkflowModel"
    jcr:uuid="6eee1ae2-6cf2-457a-a0f8-d6fbc0c96b38"
    sling:resourceType="cq/workflow/components/model"
    description="This workflow updates language copies variations for assets"
    title="DAM Update Variation Language Copy">
    <metaData
        cq:generatingPage="/conf/global/settings/workflow/models/dam/dam-update-variation-language-copy/jcr:content"
        cq:lastModified="{Long}1612297043045"
        cq:lastModifiedBy="admin"
        jcr:primaryType="nt:unstructured"/>
    <nodes jcr:primaryType="nt:unstructured">
        <node0
            jcr:primaryType="cq:WorkflowNode"
            title="Start"
            type="START">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node0>
        <node1
            jcr:primaryType="cq:WorkflowNode"
            description="Updates language copy of assets"
            title="Update Language Copy Variation"
            type="PROCESS">
            <metaData
                jcr:primaryType="nt:unstructured"
                PROCESS="com.wcm.site.workflow.ContentFragmentUpdateVariationLanguageCopyProcess"
                PROCESS_AUTO_ADVANCE="true"/>
        </node1>
        <node2
            jcr:primaryType="cq:WorkflowNode"
            title="Content Fragment Localisation WFs Starter"
            type="PROCESS">
            <metaData
                jcr:primaryType="nt:unstructured"
                PROCESS="com.wcm.site.workflow.ContentFragmentLocalisationWFStarterProcess"
                PROCESS_ARGS="modelId:/var/workflow/models/site/cf-localisation-workflow"
                PROCESS_AUTO_ADVANCE="true"/>
        </node2>
        <node3
            jcr:primaryType="cq:WorkflowNode"
            title="End"
            type="END">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node3>
    </nodes>
    <transitions jcr:primaryType="nt:unstructured">
        <node0_x0023_node1
            jcr:primaryType="cq:WorkflowTransition"
            from="node0"
            rule=""
            to="node1">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node0_x0023_node1>
        <node1_x0023_node2
            jcr:primaryType="cq:WorkflowTransition"
            from="node1"
            rule=""
            to="node2">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node1_x0023_node2>
        <node2_x0023_node3
            jcr:primaryType="cq:WorkflowTransition"
            from="node2"
            to="node3">
            <metaData jcr:primaryType="nt:unstructured"/>
        </node2_x0023_node3>
    </transitions>
</jcr:root>
See more See less

That’s pretty much it! We’ve covered all the possible cases you’ll encounter with AEM Content Fragment link localization, so you’re all ready to go!

Author: Iryna Ason

Was this article useful for you?

Get in the know with our publications, including the latest expert blogs