Thursday, 9 September 2021

How to Enable MSM with Experience Fragments created in sites/locales folder structure?


Use Case: In AEM6.5, I’ve an experience fragment which is created in global Language masters needs to be rolled out to multiple languages at the sites level with Live copy relationship. The OOTB experience fragments does meet this use case because Experience Fragment allows us to create ‘variation’ or ‘variation as Live copy’ but both of these variations will be created under the same node level hence we can’t create Live Copy of Experience Fragments in sites locales.

PFB sample experience fragments structure:

So we need some custom solution where in MSM can be possible with Experience fragments.

Here we go!! Please find below servlet which copies the experience fragment on target locales and establish the connection. You can update/optimise the code based on your experience fragments structure/requirements.

import com.mysite.config.service.CommonConfigService;
import com.google.common.base.Splitter;
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.SlingAllMethodsServlet;
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.Node;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_METHODS;
import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_PATHS;

/**
* The aim this servlet is to establish Live Copy Sync connection for Experience Fragment.
*
*/
@Component(service = Servlet.class, property = { SLING_SERVLET_PATHS + "=/etc/services/experience-fragments/lc-utility",
SLING_SERVLET_METHODS + "=GET"})
public class XFLiveCopySyncUtilityServlet extends SlingAllMethodsServlet {
private static final Logger LOGGER = LoggerFactory
.getLogger(XFLiveCopySyncUtilityServlet.class);
@Reference
private CommonConfigService configService;
ResourceResolver resourceResolver =null;
//Experience Fragments Root Path
final static String XF_ROOT_PATH ="/content/experience-fragments/my_site";
@Override
protected final void doGet(SlingHttpServletRequest request,
SlingHttpServletResponse response) throws ServletException,
IOException {
PrintWriter out= response.getWriter();
out.println("<h2>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;Welcome to XF Sync Utility</h2></br>");
String sourcePath = request.getParameter("sourcePath");
String targetLocales = request.getParameter("targets");
out.println("&nbsp; &nbsp; &nbsp; &nbsp; Source Path:" + sourcePath +"</br></br>");
List<String> targetParentsList = getTargetParents(sourcePath, targetLocales);
out.println("&nbsp; &nbsp; &nbsp; &nbsp; List of requested Target locale paths where XF Sync is required: <br> ");
for (String str:targetParentsList) {
out.println("&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; "+str+"<br>");
}
out.println("</br>");

resourceResolver = configService.getResourceResolver();
if(sourcePath!=null){
Resource resource= null;
resource = resourceResolver.getResource(sourcePath);
if(resource!=null){
for (String targetParent:targetParentsList) {
try{
Resource targetResource = resourceResolver.copy(sourcePath,targetParent);
Resource sourceResource = targetResource;
targetResource = resourceResolver.getResource(targetResource.getPath()+"/jcr:content");
String mixTypes[]={"cq:LiveRelationship", "cq:LiveSync"};
setMixin(targetResource, mixTypes);
targetResource.getResourceResolver().commit();

Map<String,Object> properties = new HashMap<>();
properties.put("jcr:primaryType", "cq:LiveCopy");
properties.put("cq:master", sourcePath);
properties.put("cq:isDeep", true);

resourceResolver.create(targetResource, "cq:LiveSyncConfig", properties);
Iterable<Resource> childResources = sourceResource.getChildren();

for (Iterator<Resource> it = childResources.iterator(); it.hasNext(); ) {
Resource childRes = it.next();
if(!childRes.getPath().endsWith("jcr:content")){
childRes.adaptTo(Node.class).remove();
}

}
resourceResolver.commit();
response.getWriter().println("<p style=\"color:green;\">Target <b>" + targetResource.getPath().replace("/jcr:content","")+"</b> is created successfully.. <p>");
} catch (Exception e) {
e.printStackTrace();
response.getWriter().println("<p style=\"color:red;\">" + e.getMessage()+"</p>");
out.print("&nbsp;&nbsp;&nbsp;&nbsp;Please verify target parent location is present or not OR check if the page you're trying sync is already exist..</br> ");
}
}


}else{
out.print("</br></br>&nbsp;&nbsp;&nbsp;&nbsp;<style='color:red'>Sorry ! Please verify and provide valid source page path. Thank You. </style></br> ");
}

}

}
/**
* Method to get Target parents
*
@param sourcePath
*
@param targetLocales
*
@return List of targets
*/
private List<String> getTargetParents(String sourcePath, String targetLocales) throws IOException {
List<String> targetsParentsList = new ArrayList<>();
List<String> locales = Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.splitToList(targetLocales);

String[] locale = sourcePath.split("/");
String orgLocale = locale[5];
StringBuilder sourceXFParentPath = new StringBuilder();
for (int i=1; i<locale.length-1;i++) {
sourceXFParentPath = sourceXFParentPath.append("/"+locale[i]);

}
String targetRoot = null;
targetRoot = sourceXFParentPath.toString().startsWith(XF_ROOT_PATH+"/global/")?
sourceXFParentPath.toString().replace(XF_ROOT_PATH+"/global", XF_ROOT_PATH+"/sites"): sourceXFParentPath.toString() ;

for (String targetLocale:locales) {
targetsParentsList.add(targetRoot.replace(orgLocale,targetLocale));

}
return targetsParentsList;
}

/**
* Set mixin type for the given resource
*
@param post
*
@param mixinTypes
*/
private void setMixin(Resource post, String [] mixinTypes) {
try {
if(post !=null){
Node postNode = post.adaptTo(Node.class);
postNode.addMixin("cq:LiveRelationship");
}
} catch (Exception e) {
LOGGER.error("Exception while getting the node :: {}",e.getMessage(),e);
}
}
}

Step 1: run the servlet

URL: http://localhost:6502/etc/services/experience-fragments/lc-utility?sourcePath=<experience-fragment-path>&targets=<sites level locales with comma separated>

Example: http://localhost:6502/etc/services/experience-fragments/lc-utility?sourcePath=/content/experience-fragments/my-site/global/en_nl/XF-MSM-LCDemo&targets=en_nl,en_pt,en_pl

As a result, It copies experience fragment (Eg: XF-MSM-LCDemo) to sites level en_nl, en_pt and en_pl .

Example screenshots:

Note: The servlet will not copy experience fragment if the experience fragment is already present on target locale.

i.e, if you try to run the same servlet multiple times on same source path, then you will see below response from servlet.

Step 2: Go to global experience fragment which is http://localhost:8502/editor.html/content/experience-fragments/my_site/global/eu_nl/xf-msm-lcdemo/master.html as per the example and rollout the variation(s).

You’re done, Now you can go back and verify the experience fragment on target locales and see how it works.

Thanks for reading!!