AEM Content Fragments: Links Localization. Part 2
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);
}
}
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>
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);
}
}
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>
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
End-to-End Digital Transformation
Reach out to our experts to discuss how we can elevate your business