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!!


Saturday 6 September 2014

CQ Component Dialog

<?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:Dialog" activeTab="0" title="Component Title" xtype="tabpanel"> <items jcr:primaryType="cq:WidgetCollection"> <tab1 jcr:primaryType="cq:Widget" title="Tab 1" xtype="panel"> <items jcr:primaryType="cq:WidgetCollection"> <checkbox jcr:primaryType="cq:Widget" fieldLabel="Checkbox" fieldDescription="" defaultValue="false" inputValue="true" name="./checkbox" type="checkbox" xtype="selection"/> <cqinclude jcr:primaryType="cq:Widget" path="/path/to/other/dialog/inputs.infinity.json" xtype="cqinclude"/> <datetime jcr:primaryType="cq:Widget" fieldLabel="Date Time" fieldDescription="" name="./dateTime" allowBlank="{Boolean}true" xtype="datetime"/> <dialogfieldset jcr:primaryType="cq:Widget" collapsed="{Boolean}true" collapsible="{Boolean}true" title="Fieldset Title" xtype="dialogfieldset"> <items jcr:primaryType="cq:WidgetCollection"> </items> </dialogfieldset> <displayfield jcr:primaryType="cq:Widget" ignoreData="{Boolean}true" hideLabel="{Boolean}true" hidden="{Boolean}false" value="Escaped HTML or plain-text to display" xtype="displayfield"/> <dropdown jcr:primaryType="cq:Widget" fieldLabel="Dropdown" fieldDescription="" defaultValue="" name="./dropdown" type="select" xtype="selection"> <options jcr:primaryType="cq:WidgetCollection"> <children jcr:primaryType="nt:unstructured" text="Option 1" value="option1"/> <descendants jcr:primaryType="nt:unstructured" text="Option 2" value="option2"/> </options> </dropdown> <html5smartfile jcr:primaryType="cq:Widget" autoUploadDelay="1" ddGroups="[media]" fieldLabel="HTML 5 Smart File" fieldDescription="" fileNameParameter="./fileName" fileReferenceParameter="./fileReference" name="./file" xtype="html5smartfile"/> <inlinetextfield jcr:primaryType="cq:Widget" fieldLabel="Inline Text Field" fieldDescription="" name="./inlineTextField" regex="(.*)" regexText="Message If Regex Fails" xtype="inlinetextfield"/> <ownerdraw jcr:primaryType="cq:Widget" fieldLabel="Owner Draw" fieldDescription="" html="HTML fragment to use as the owner draw's body content" name="./ownerDraw" url="The URL to retrieve the HTML code from. Replaces HTML defined in html." xtype="ownerdraw"/> <paragraphreference jcr:primaryType="cq:Widget" fieldLabel="Paragraph Reference" fieldDescription="" name="./paragraphReference" regex="(.*)" regexText="Message If Regex Fails" xtype="paragraphreference"/> <multifield jcr:primaryType="cq:Widget" fieldLabel="Multifield" fieldDescription="Click the '+' to add a new page" name="./multifield" xtype="multifield"> <fieldConfig jcr:primaryType="cq:Widget" width="155" xtype="pathfield"/> </multifield> <pathfield jcr:primaryType="cq:Widget" fieldLabel="Pathfield" fieldDescription="Drop files or pages from the Content Finder" name="./pathfield" regex="(.*)" regexText="Message If Regex Fails" rootPath="/content" suffix=".html" showTitlesInTree="{Boolean}true" typeAhead="{Boolean}true" xtype="pathfield"/> <radiobuttons jcr:primaryType="cq:Widget" fieldLabel="Radio button" fieldDescription="Field Description" name="./radioButton defaultValue="option1" type="radio" xtype="selection"> <options jcr:primaryType="cq:WidgetCollection"> <option1 jcr:primaryType="nt:unstructured" text="Option 1" value="option1"/> <option2 jcr:primaryType="nt:unstructured" text="Option 2" value="option2"/> </options> </radiobuttons> <resType jcr:primaryType="cq:Widget" ignoreData="{Boolean}true" name="./sling:resourceType" value="some/resource/type" xtype="hidden"/> <richtext jcr:primaryType="cq:Widget" fieldLabel="Rich Text" fieldDescription="" defaultValue="" hideLabel="{Boolean}true" name="./richText" xtype="richtext"> <rtePlugins jcr:primaryType="nt:unstructured"> <table jcr:primaryType="nt:unstructured" features="*"/> </rtePlugins> </richtext> <searchfield jcr:primaryType="cq:Widget" fieldDescription="" fieldLabel="Search Field" name="./searchField" regex="(.*)" regexText="Message If Regex Fails" url="The URL where the search request is sent to (defaults to "/content.search.json")" xtype="searchfield"/> <sizefield jcr:primaryType="cq:Widget" fieldLabel="Sizefield" fieldDescription="" heightParameter="./height" widthParameter="./width" heightSuffix="px" widthSuffix="px" xtype="sizefield"/> <textarea jcr:primaryType="cq:Widget" fieldLabel="Textarea" name="./textarea" grow="{Boolean}true" regex="(.*)" regexText="Message If Regex Fails" xtype="textarea"/> <textfield jcr:primaryType="cq:Widget" fieldLabel="Textfield" name="./textfield" xtype="textfield"/> </items> </tab1> <tab2 jcr:primaryType="cq:Widget" cropParameter="./image/imageCrop" ddGroups="[media]" fileNameParameter="./image/fileName" fileReferenceParameter="./image/fileReference" mapParameter="./image/imageMap" name="./image/file" requestSuffix="/image.img.png" rotateParameter="./image/imageRotate" sizeLimit="100" title="Image" xtype="html5smartimage"/> </items> </jcr:root>

OSGi Service Declaration

@Component( label = "Service name", description = "Service description", metatype = true, immediate = false) @Properties({ @Property( label = "Vendor", name = Constants.SERVICE_VENDOR, value = "Customer", propertyPrivate = true ) }) @Service public class SampleServiceImpl implements SampleService { private final Logger log = LoggerFactory.getLogger(this.getClass()); /** * OSGi Properties * */ private static final boolean DEFAULT_SAMPLE = false; private boolean enabled = DEFAULT_SAMPLE; @Property(label = "Prop name", description = "Prop description", boolValue = DEFAULT_SAMPLE) public static final String PROP_SAMPLE = "prop.sample"; ... @Activate protected void activate(final Map<String, String> config) { } @Deactivate protected void deactivate(final Map<String, String> config) { } }

How to define Sling Servlet?

Sling Servlet annotations

Configurations methodsresourceTypesselectorsextensions are IGNORED if paths is set.
Methods defaults to GET if not specified.

@SlingServlet( label = "Samples - Sling Servlet", description = "...", paths = {"/services/all-sample"}, methods = {"GET", "POST"}, resourceTypes = {}, selectors = {"print.a4"}, extensions = {"html", "htm"} )

Safe Methods Servlet (GET, HEAD)

public class SampleServlet extends SlingSafeMethodsServlet implements OptingServlet { ... }

All Methods Servlet (GET, HEAD, POST, PUT, DELETE)

public class SampleServlet extends SlingAllMethodsServlet implements OptingServlet { ... }

How to start workflow programmatically?

Start Workflow programmatically

@Reference WorkflowService wfService; ... WorkflowSession wfSession = wfService.getWorkflowSession(jcrSession); WorkflowModel model = wfSession.getModel("/etc/workflow/models/x/jcr:content/model"); WorkflowData data = wfSession.newWorkflowData("JCR_PATH", payloadPath); Workflow workflow = wfSession.startWorkflow(model, data);


Difference between Dialog & Design dialog in CQ


Design Dialog:  It is used to globally store variables throughout  the template properties.

Dialog:  Stores all variables inside the page’s properties


One of the major benefits of using the design dialog is that if you have a hundred pages sharing the same template the variables will be shared amongst them. Also, note that you can have both design dialog and normal dialog on a page.
A dialog saves content relative to the given page.  In comparison, a 'design' dialog saves content globally (at a template level).

How to reuse existing dialog elements into other component's dialog?

How is it possible to reuse existing dialog elements such as tabs in other dialogs?

For this use-case, a specific widget with the label cqinclude exists which allows for inclusion of existing dialog elements in other dialog definitions. The generic JSON format is used on the client side to construct the actual dialog.
Following is an example which uses the cqinclude widget to include an existing tab from an existing dialog:
{
  "jcr:primaryType": "cq:Widget",
  "xtype": "cqinclude",
  "path": "/libs/replication/components/agent/tab_extended.infinity.json"
}
The path property needs to point to a dialog-resource that is to be included in JSON format.
The above example was taken from the reverse-replication agent component (please refer to the/libs/replication/components/revagent node).