Showing posts with label AEM. Show all posts
Showing posts with label AEM. Show all posts

Wednesday, 1 January 2020

A servlet to Query Pages containing particular components.

If you want to get your list of pages having particular components authored in it inside your project content folder in the form of json response, then here's a servlet for you.

The servlet is registered at path - /bin/generate/pagesreport to which you need to pass your page folder path (default is /content) and components path (separated by comma) as a query-param and it will output the list of pages (its title, path, template and component resourceType) as a json response.

What this servlet is doing is - it is forming a Predicate Map of Query having-
- path (content path from the input you provide)
- node type (nt:unstructured) 
- property (sling:resourceType)
- property value (list of components path from the input you provide)
and using it in QueryBuilder API and fetching the results in form of page node paths.

Further it is processing the hits (page node paths) from the search result to form the json response which you can consume by dong an ajax call.

This is the servlet for it-
package com.aem.community.wf.core.servlets;

import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
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.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;

import javax.jcr.Session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@SlingServlet(paths = "/bin/generate/pagesreport")
public class PageQueryServlet extends SlingSafeMethodsServlet {

 private static final long serialVersionUID = 1L;
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
 @Reference
 private QueryBuilder builder;
 
 @Reference
 private ResourceResolverFactory resolverFactory;
 
 @SuppressWarnings("deprecation")
 @Override
 protected void doGet(SlingHttpServletRequest request, 
                 SlingHttpServletResponse response) throws IOException {
  
     String pageRootPath = request.getParameter("pageroot");   
     if(pageRootPath == null){
      pageRootPath = "/content";
     }
     String compType = request.getParameter("comptype");   
     if(compType == null){
      compType = "weretail/components/content/productgrid";
     }else{
      compType = compType.replace("/apps/", "");
      logger.info("Logging comp type is: "+compType);
     }
     long totalMatches = 0;
     
     response.setCharacterEncoding("UTF-8");
     response.setContentType("application/json");
     
     JSONObject json = new JSONObject();
     JSONArray jsonarray = new JSONArray();
     JSONObject tempJson;
     
     try { 
         ResourceResolver resolver = request.getResourceResolver();
         Session session = resolver.adaptTo(Session.class);
      
         Map map = new HashMap();             
         map.put("path", pageRootPath);
         map.put("type", "nt:unstructured");
         map.put("property", "sling:resourceType");
         
         String[] patharr = compType.split(",");
         for (int i=0; i<patharr.length; i++){
             map.put("property."+i+"_value", patharr[i]);
         }
         map.put("p.offset", "0");
         map.put("p.limit", "-1");
                           
         Query query = builder.createQuery(PredicateGroup.create(map), session);                    
                     
         SearchResult result = query.getResult();
         totalMatches = result.getTotalMatches();                             
         String hitpath;
         Resource pageResource;
         String pagepath;
         String jcrpagepath;
         List<String> pagesList = new ArrayList<String>();
         int pagecounter = 0;
         int compcounter = 1;
         
         for (Hit hit : result.getHits()) {
            hitpath = hit.getPath();   
            tempJson = new JSONObject();
            pagepath = hitpath.split("/jcr:content")[0];
            jcrpagepath = pagepath +"/jcr:content";
            pageResource = resolver.getResource(jcrpagepath); 
                
            if (pageResource != null && !pagesList.contains(pagepath)) { 
              pagecounter++;
              ValueMap valueMap = pageResource.getValueMap(); 
              tempJson.put("pagename", 
                            valueMap.get("jcr:title", String.class));
              tempJson.put("pagerestype", 
                            valueMap.get("sling:resourceType", String.class));
              tempJson.put("pagetemplate", 
                            valueMap.get("cq:template", String.class));
              tempJson.put("pagepath", pagepath);
              jsonarray.put(tempJson);
              pagesList.add(pagepath);
            }
         }
         json.put("totalcomponents", totalMatches);
         json.put("totalpages", pagecounter);
         json.put("pages", jsonarray);
         logger.info("Total" + totalMatches + "paths were found: \n");
         
     } catch (Exception e) {
      logger.error("Exception caught: "+e);
 }
     response.getWriter().print(json.toString());
    
    }
}

Below is the sample url to call this servlet by passing apps folder path-
http://localhost:4502/bin/generate/pagesreport?pageroot=/content&comptype=weretail/components/content/productgrid,weretail/components/content/text

And this is the screenshot of the response by hitting above url-

A servlet to Query Components inside your apps folder.

If you want to get your list of components inside your project apps folder in the form of json response, then here's a servlet for you.

The servlet is registered at path - /bin/generate/componentsreport  to which you need to pass your project folder path (default is /apps) as a query-param and it will output the list of components (their name and path) as a json response.

What this servlet is doing is - it is forming a Predicate Map of Query having the path (apps path from the input you provide) and node type (cq:Component) and using it in QueryBuilder API and fetching the results in form of component paths.
Further it is processing the hits (components path) from the search result to form the json response which you can consume by dong an ajax call.

This is the servlet for it-
package com.aem.community.wf.core.servlets;

import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
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.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;

import javax.jcr.Session;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@SlingServlet(paths = "/bin/generate/componentsreport")
public class ComponentQueryServlet extends SlingSafeMethodsServlet {

 private static final long serialVersionUID = 1L;
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
 @Reference
        private QueryBuilder builder;
 
 @Reference
        private ResourceResolverFactory resolverFactory;
 
 @SuppressWarnings("deprecation")
 @Override
        protected void doGet(SlingHttpServletRequest request, 
                SlingHttpServletResponse response) throws IOException {
        
  
     String compPath = request.getParameter("comppath");   
     if(compPath == null){
      compPath = "/apps";
     }
     long totalMatches = 0;
     
     response.setCharacterEncoding("UTF-8");
 response.setContentType("application/json");
     
     JSONObject json = new JSONObject();
     JSONArray jsonarray = new JSONArray();
     JSONObject tempJson;
     
     try { 
      ResourceResolver resolver = request.getResourceResolver();
      Session session = resolver.adaptTo(Session.class);
      
      Map map = new HashMap();             
         map.put("path", compPath);
         map.put("type", "cq:Component");
         map.put("p.offset", "0");
         map.put("p.limit", "-1");
                           
         Query query = builder.createQuery(PredicateGroup.create(map),
                                                                     session);                    
                     
         SearchResult result = query.getResult();
         totalMatches = result.getTotalMatches();                             
         String hitpath;
         Resource compResource;
         String compTitle;
         
         for (Hit hit : result.getHits()) {
                hitpath = hit.getPath();   
                tempJson = new JSONObject();
                
                compResource = resolver.getResource(hitpath); 
         if (compResource != null) { 
                    ValueMap valueMap = compResource.getValueMap(); 
                    compTitle = valueMap.get("jcr:title", String.class);
                    tempJson.put("componentname", compTitle);
                }
         tempJson.put("componentpath", hitpath);
         jsonarray.put(tempJson);
         }
         json.put("totalcomponents", totalMatches);
         json.put("components", jsonarray);
         logger.info("Total" + totalMatches + "paths were found: \n");
         
     } catch (Exception e) {
      logger.error("Exception caught: "+e);
 }
     response.getWriter().print(json.toString());
    
    }
}

Below is the sample url to call this servlet by passing apps folder path-
http://localhost:4502/bin/generate/componentsreport?comppath=/apps/weretail

And this is the screenshot of the response by hitting above url-



  

A servlet to output html markup template of your AEM page.

If you want to get the html markup template of your AEM page which doesn't have css or js and pure html with white-spaces stripped off, here's a servlet for you.

You can invoke this servlet in any of your aem page by using the page path with 'template' selector and it will output the plain html for you.
Ex: http://localhost:4502/content/we-train/en.template.html

In few of the projects, when aem is integrated with SPA framework, where the role of aem is just hosting pages with structure and content and the client-side rendering is done through external SPA framework, it is often required to deliver the html from aem to front-end SPA team. In those cases, this servlet can be utilized.

So below is the AEM servlet registered with default resourceType - sling/servlet/default, and selector as template, so it can be invoked on any page.
I have written the comments as well in the code for understanding what each piece is doing. 

package com.foo.community.core.servlets;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.engine.SlingRequestProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.wcm.api.WCMMode;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * A default resourceType servlet called with 'template' selector to output
 * html template without css and js
 */
@SlingServlet(methods = {"GET"},
resourceTypes = {"sling/servlet/default"},
selectors = {"template"})
public class TemplateServlet extends SlingSafeMethodsServlet {

    private static final long serialVersionUID = 1L;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
    /** Service to create HTTP Servlet requests and responses */
    @Reference
    private RequestResponseFactory requestResponseFactory;
    
    /** Service to process requests through Sling */
    @Reference
    private SlingRequestProcessor requestProcessor;

    @Override
    protected void doGet(final SlingHttpServletRequest request,
            final SlingHttpServletResponse response) throws ServletException,
              IOException {
        
        String requestPath = request.getRequestPathInfo().getResourcePath() +
             "." + request.getRequestPathInfo().getExtension();
        logger.info("requestPath is : "+requestPath);
  
        /** Setup request */
        HttpServletRequest req = 
                        requestResponseFactory.createRequest("GET", requestPath);
        WCMMode.DISABLED.toRequest(req);

        /** Setup response */
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        HttpServletResponse resp = requestResponseFactory.createResponse(out);

        /** Process request through Sling */
        requestProcessor.processRequest(req, resp, request.getResourceResolver());
        String htmlcontent = out.toString();
     
        /** Replacing js and css files extension in html */
        String content = htmlcontent.replace(".js", ".nojs");
        content = content.replace(".css", ".nocss");
  
        /** Setting up writer to write html template */
         PrintWriter writer = null;
        if (StringUtils.isNotBlank(content)) {
            content = content.replaceAll("(\r?\n+\t?\\s*)", "\n");
            content = content.replaceAll("\n", ""); 
            logger.info("content written is :\n"+content);
            response.setCharacterEncoding("UTF-8");  
            response.setContentType("text/html");  
            writer = response.getWriter();
            writer.write(content);
            writer.close();
        }
    }
}

Below is the screenshot of how any page looks like in aem author-



and by using template selector on the above page, you will get it's html output-

Sunday, 15 September 2019

Open infinity.json of your AEM page with Bookmarklet

In AEM we are often required to open .infinity.json of any page specially in author mode to see the node data tree structure in json format. infinity selector with json extension is provided by AEM OOTB for such use cases.

This is also helpful when you are debugging something related to content authored and don't want to open the page content structure in CRXDE and open each node manually till you reach that particular component node inside jcr:content of the page because it is time consuming.

Furthermore if you use .tidy.infinity.json, it will write your json data in neat and tidy way (See SS at the bottom of the post). tidy selector is also provided OOTB by AEM for tidy representation.

To use this we usually copy the editor url in new tab and append .tidy.infinity.json to the url and remove .html extension. How about if you can do this in just 1 click?

The answer is bookmarklet. A bookmarklet is a bookmark stored in a browser but instead of having a page url, it contains the JavaScript code in its URL. On click of these bookmarks, the JavaScript code executes onto your page and can modify it similar to what you would do from developer console.

So i created this very simple bookmarklet which you can run on any editor page and open infinity.json of that page in new tab in just 1 click.

javascript:(function(){ 
    var currurl = window.location.href; 
    if(currurl.indexOf('editor.html') > -1){ 
        var arr = currurl.split('.html'); 
        var newurl = arr[0].replace('/editor','')+arr[1]+'.tidy.infinity.json'; 
        window.open(newurl , '_blank'); 
    }
})();

The above logic only works in author mode as you can see i am checking if current url contains editor.html. You can modify this as per your requirement to work in publish as well.

How to use this?  

1. Click on Add page from bookmarks toolbar (similar to what you do to add any bookmark).
2. Give any Name and in the URL field copy the above code (See SS below)-



3. That's it. Now your bookmarklet is ready. To run on any page, just click on it and it will open its infinity json for yo in new tab.

See screenshots below for reference-


Thursday, 17 January 2019

Touch UI - Hide/Show fields inside coral-3 multifield in AEM by coral dropdown

This post shows how to hide/show fields inside multifield in the Touch UI dialog of AEM.
The resourceType used here for composite multifield, selection dropdown, textfield and numberfield are coral 3 ui components. For ex: granite/ui/components/coral/foundation/form/multifield and it has been tested in aem 6.3.

This method uses granite:class attribute added as property to add class in the fields and granite:data node with data key-value added as property to add data attributes in the fields.
Implemented 4 steps are mentioned below: 

1. Add granite:class attribute to the selection field as shown below
 granite:class : cq-dialog-dropdown-showhide1


2. Add granite:data node inside selection field with data key-value added as property as shown below:
cq-dialog-dropdown-showhide-target1 : .list-option-listfrom-showhide-target



3. Add granite:class attribute to each of the fields which you want to show/hide based on 
dropdown selection which matches the data-target attribute of selection field as shown below:
granite:class : list-option-listfrom-showhide-target1


4. Add granite:data node inside each of the fields which you want to show/hide with showhidetargetvalue key 
and value equal to dropdown options value as shown below:
  showhidetargetvalue : textval



To avoid any confusion, the cq_dialog xml i created is pasted below-

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" 
xmlns:granite="http://www.adobe.com/jcr/granite/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="nt:unstructured"
    jcr:title="Multi ShowHide Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="https://www.adobe.com/go/aem6_3_docs_component_en#Image - HTL">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/tabs"
            type="nav"/>
        <items jcr:primaryType="nt:unstructured">
            <multi
                jcr:primaryType="nt:unstructured"
                jcr:title="Multi"
                sling:resourceType="granite/ui/components/foundation/section">
                <layout
                    jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                    margin="{Boolean}false"/>
                <items jcr:primaryType="nt:unstructured">
                    <custom
                        jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/container">
                        <items jcr:primaryType="nt:unstructured">
                            <heading
                                jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/heading"
                                class="coral-Heading coral-Heading--4"
                                level="{Long}4"
                                text="Composite Multifield"/>
                            <well
                                jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/container">
                                <layout
                                    jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/foundation/layouts/well"/>
                                <items jcr:primaryType="nt:unstructured">
                                    <fieldenter
                                        jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
                                        composite="{Boolean}true">
                                        <field
                                            jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/coral/foundation/container"
                                            fieldLabel="Products Container"
                                            name="./products">
      <items jcr:primaryType="nt:unstructured">
                                            <listFromMulti
      granite:class="cq-dialog-dropdown-showhide1"
                                            jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/coral/foundation/form/select"
                                            fieldLabel="Select Multi showhide"
                                            name="listFromMulti">
      <items jcr:primaryType="nt:unstructured">
                                            <text
                                            jcr:primaryType="nt:unstructured"
                                            text="Text field"
                                            value="textval"/>
                                            <num
                                            jcr:primaryType="nt:unstructured"
                                            text="Num field"
                                            value="numval"/>
                                            </items>
                                            <granite:data
                                            jcr:primaryType="nt:unstructured"
      cq-dialog-dropdown-showhide-target1=".list-option-listfrom-showhide-target1"/>
                                            </listFromMulti>
                                            <multitext
      granite:class="list-option-listfrom-showhide-target1"
                                            jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                              fieldLabel="Multi Text"
                                              name="multitext"
                                              showhidetargetvalue="textval">
                                              <granite:data
                                                jcr:primaryType="nt:unstructured"
                                                showhidetargetvalue="textval"/>
                                              </multitext>
                                              <multinum
      granite:class="list-option-listfrom-showhide-target1"
                                              jcr:primaryType="nt:unstructured"
      sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
                                              fieldLabel="Multi Num"
                                               name="multinum"
                                               showhidetargetvalue="numval">
                                               <granite:data
                                               jcr:primaryType="nt:unstructured"
                                               showhidetargetvalue="numval"/>
                                               </multinum>
                                            </items>
                                        </field>
                                    </fieldenter>
                                </items>
                            </well>
                        </items>
                    </custom>
                </items>
            </multi>
        </items>
    </content>
</jcr:root>

Now to toggle fields using selection dropdown inside composite multifield independently, use below jquery logic 
written in a js file inside clientlibs folder of component with category cq.authoring.dialog .
 
(function(document, $) {
    "use strict";

    // when dialog gets injected
    $(document).on("foundation-contentloaded", function(e) {
        // if there is already an inital value make sure the 
  //according target element becomes visible
        showHideHandler($(".cq-dialog-dropdown-showhide1", e.target));
    });

    $(document).on("change", ".cq-dialog-dropdown-showhide1", function(e) {
        showHideHandler($(this));
    });

    function showHideHandler(el) {
        el.each(function(i, element) {
                // handle Coral3 base drop-down
                Coral.commons.ready(element, function(component) {
                    showHideCustom(component, element);
                    component.on("change", function() {
                        showHideCustom(component, element);
                    });
                });
        })
    }

    function showHideCustom(component, element) {
         // get the selector to find the target elements. 
         //its stored as data-.. attribute
       var target = $(element).data("cq-dialog-dropdown-showhide-target1");
       var $target = $(target);
       var elementIndex = $(element).closest('coral-multifield-item').index();

       if (target) {
         var value;
         if (component.value) {
           value = component.value;
         } else {
           value = component.getValue();
         }
         $(element).closest("coral-multifield-item").find(target)
         .each(function(index) {
            var tarIndex = $(this).closest('coral-multifield-item').index();
            if (elementIndex == tarIndex) {
                $(this).not(".hide").parent().addClass("hide");
                $(this).filter("[data-showhidetargetvalue='" + value + "']")
                .parent().removeClass("hide");
            }
         });
        }
    }

})(document, Granite.$);

The screenshot of how the dialog looks like and how it toggle fields is shown below- 


You can also find this component in my github project here .

Sunday, 17 June 2018

Content Copier Servlet to perform JCR copy operation

This post is a part-2 of this post. In my previous post i have created a servlet invoker utility component and I will use that tool to invoke the Content copier servlet which is demonstrated below.

This servlet performs the JCR workspace copy operation. To demonstrate a particular use case i have worked on, there was a bunch of pages in a folder of type (sling:OrderedFolder) in CQ5 inside /content and more sub-pages inside the sub folder of type (sling:OrderedFolder) in it, and the ask was to migrate the pages from folder structure (nested) in CQ5 to AEM page structure.

To achieve the same I have written a servlet which accepts the source(path of the parent source folder) and destination (path of the parent destination page) paths as arguments and perform the workspace copy operation.

The primary method used to perform the operation is
Workspace.copy(String srcAbsPath, String destAbsPath)
This method copies the subgraph rooted at, and including, the node at srcAbsPath to the new location at destAbsPath.

Servlet to achieve the functionality-
package com.foo.community.core.servlets;

import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;

import javax.jcr.Repository;
import javax.servlet.ServletException;
import java.io.IOException;
import javax.jcr.NodeIterator;

import javax.jcr.Session; 
import javax.jcr.Node; 
import javax.jcr.Workspace;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 


@SuppressWarnings("serial")
@SlingServlet(paths = "/system/utility/servletinvoker")
public class ContentCopierServlet extends SlingSafeMethodsServlet {

  private static final Logger log = LoggerFactory.getLogger(this.getClass());

  @Reference
  private Repository repository;
  @Override
  protected void doGet(final SlingHttpServletRequest req,
   final SlingHttpServletResponse resp) throws ServletException, IOException {

     String message="Initial servlet message";
     String requestPath1 = req.getParameter("path1");
     String requestPath2 = req.getParameter("path2");
     try {
 Session session= req.getResourceResolver().adaptTo(Session.class);
 copyNodes(requestPath1,requestPath2,session);
 message= "Content copied from: "+ requestPath1+ " to: "+ requestPath2; 
     } 
     catch (Exception e) {
 message="Error performing operation: " + e.getMessage();
 log.error("copyServlet main error", e);
 e.printStackTrace();
     }
     resp.getWriter().println(message);
    }
 
   private void copyNodes(String requestPath1, String requestPath2, Session session)
                                                              throws Exception{
 
    Workspace workspace = session.getWorkspace();
    String resrc = requestPath1.substring(1,requestPath1.length()); 
    String redes = requestPath2.substring(1,requestPath2.length()); 
  
    Node rootnode = session.getRootNode();
    Node srcnodepath = rootnode.getNode(resrc);
    Node destnode = rootnode.getNode(redes);
  
    if (srcnodepath.hasNodes()) {
 NodeIterator worknodes = srcnodepath.getNodes();
 while (worknodes.hasNext()) {
    Node childnde = worknodes.nextNode();
    
    // formation of destination path and its relative path variables
    String[] parts = childnde.getPath().split("/");
    String destrelativepath = parts[parts.length-1];
    String destpath = destnode.getPath() + "/" + destrelativepath;
    
    if (destnode.hasNode(destrelativepath)) {
    
  String prtype= childnde.getProperty("jcr:primaryType").getString();
  log.info("primary type is: "+prtype);
  int flag=0;
  if(prtype.equals("sling:OrderedFolder")){
   copyNodes(childnde.getPath(),destpath,session);
   flag=1;
  }     
     
  if(!destpath.contains("jcr:content") && flag==0){
   destnode.getNode(destrelativepath).remove();
   session.save();
   log.info("src path is: "+childnde.getPath());
   log.info("dest path is: "+destpath);
   workspace.copy(childnde.getPath(), destpath);
  }
    }else{
  String srpath= childnde.getPath();
  workspace.copy(srpath, destpath);  
           }
 }
       }
     } 
}


Before beginning the operation, make sure to have the destination page structure(skeleton) ready for this to work.
All you have to do is to create the destination pages corresponding to folders of your source.
If your source contains the sub-folders, you have to create a nested page structure in your destination also. 
The screenshot of CRX before and after the operation is shown below:


The Servlet is registered as path,  so you can make an ajax call from your utility component and pass the source and destination paths as an argument.
Also, mostly the utilities like these are performed on author instance so you can find the proper path for the servlet to be registered at.

So, this was my use case, but if you have similar such requirement, you know how to do JCR workspace copy operation.

Wednesday, 13 June 2018

Servlet Invoker Utility Component

This post will be a beginning of various posts in which I will create a Servlet Invoker utility component and through this component invoke the servlet for various operations in AEM by passing parameters from our component as an argument to the servlet.

Often times, we create a utility in AEM in form of Services, Servlets etc. and call them by various means using Sling Models, WCMUSEPojo etc. which is not consistent though it achieves your functionality, but here through a common component we can invoke our Servlets and pass parameters to it and achieve our functionality.

To begin with, create a component in AEM and it's dialog. The dialog can contains the field for which servlet to invoke and what parameters to pass to the Servlet .
It's html contains a simple Button on press of which our servlet will invoke.

For ex:
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html">

   <h2>Welcome to Servlet Invoker Utility Component</h2>

   <strong>Source path:</strong> <span id="path1">${properties.path1}</span> <br>
   <strong>Destination path:</strong> <span id="path2">${properties.path2}</span>
   <br><br>

   <input id="submitButton" type="submit" title="Submit" value="Begin Operation">

   <div id="result">
       <h3 data-sly-test="${properties.path1 && properties.path1}">
                        Press Begin Operation button to invoke servlet</h3>
       <h3 data-sly-test="${!(properties.path1 && properties.path1)}">
                        Please author source and destination path</h3>
   </div>

<sly data-sly-call="${clientlib.js @ categories='servlet.invoke'}" />
It looks something like this:















Along with the html file of the component, create a clientlibs for the component with your categories say servlet.invoke and write a JS code which contains a logic to do an ajax call to your servlet on press of the Button. Also you can pass the parameters to your servlets through the same ajax call which you can get from your html.
For ex.
(function ($, document) {
    'use strict';
    $("#submitButton").click(function(){
        var r = confirm("This will invoke the servlet and begin operation.
                         Do you want to proceed? ");
        if (r == true) {
            $("#result").html('<h3>
                          Operation in progress...Please wait</h3>');
            $.ajax({
                    type : "GET",
                    url : '/system/utility/servletinvoker',

                    data : {
                        path1: $("#path1").html(), path2:$("#path2").html()
                    },
                    success : function(data) {

                        $("#result").html('<h3>
                                          '+data+'</h3>');
                    },
                    error : function(XMLHttpRequest, textStatus, errorThrown) {
                        $("#result").html('<h3>
                    Error calling utility servlet: '+errorThrown+'</h3>');
                    }
            });}});

})(Granite.$, document);
Notice the url of the servlet for ajax call, by this you can understand that the servlet is registered via path, Since most probably, you will be using this utility component in author instance, you can find out a path for the servlet to be registered in AEM.

Now, you are good to go with your simple component. In my next post, I'll create a servlet and show you how to invoke it through this component.

Sunday, 20 May 2018

OSGI Service Configuration and its resolution order

OSGI is a fundamental element in the technology stack of Adobe Experience Manager (AEM). It is used to control the bundles of AEM and their configuration.

OSGI bundles or components can be composed into an application and deployed and they can be stopped, installed, started individually. The interdependencies are handled automatically. Each OSGI Component is contained in one of the various bundles.

In the cases when you don't want to hardcode the values in the code eg. some host url or some parameter which is dynamic and can be changed frequently or the value which is different for each runmode of your aem instance, in those cases you use OSGI configuration.

By defining configuration values, you can define values used by an OSGI service and use these values while the service is running. Unlike hard-coding values in an AEM OSGI service, defining values in the AEM configMgr lets you change values without re-compiling the bundle.

You can manage the OSGI configuration settings for such bundles by :

  • Adobe CQ Web console - The Web Console is the standard interface for OSGI configuration. It provides a UI for editing the various properties, where possible values can be selected from predefined lists. Any configurations made with the Web Console are applied immediately and applicable to the current instance, irrespective of the current run mode.

  • Content-nodes (sling:osgiConfig) in the repository - This requires manual configuration using CRXDE Lite. You can create sling:OsgiConfig nodes according to a specific run mode in different config folders. You can even save configurations for more than one run mode in the same repository. Any appropriate configurations are applied immediately (dependent on the run mode).

  • Creating a OSGI Configuration service - 


    Use the below Java class example to create an osgi config service:
    package com.foo.community.core;
    
    import java.util.Map;
    import org.apache.felix.scr.annotations.Activate;
    import org.apache.felix.scr.annotations.Component;
    import org.apache.felix.scr.annotations.Deactivate;
    import org.apache.felix.scr.annotations.Modified;
    import org.apache.felix.scr.annotations.Property;
    import org.apache.felix.scr.annotations.Service;
    import org.apache.sling.commons.osgi.PropertiesUtil;
    import org.osgi.service.component.ComponentContext;
    
    
    @Component(immediate=true, metatype=true)
    @Service({MyConfig.class})
    public class MyConfig
    {
      @Property(label="Sample Property", value={"property1"}, 
        description="Sample property to demonstrate osgi config")
      private static final String SAMPLE_PROPERTY = "sampleProperty";
      private String sampleProperty;
      
      public MyConfig() {}
      
      public String getSampleProperty()
      {
        return sampleProperty;
      }
      
      @Activate
      protected void activate(Map context)
      {
       sampleProperty = PropertiesUtil.toString(context
         .get("sampleProperty"), "property1");
      }
      
      @Deactivate
      protected void deactivate()
      {
       sampleProperty = null;
      }
      
      @Modified
      protected void modified(ComponentContext context)
      {
       sampleProperty = PropertiesUtil.toString(context
          .getProperties().get("sampleProperty"), "property1");
      }
    }
    
    
    Notice that in the above code I have used SCR annotations to create a service but those annotations are deprecated now, so you can use OSGI annotations instead.
    To know more about scr annotations refer this page.

    OSGi Configuration with the Web Console -

    You can access the Configuration tab of the Web Console by:
    http://localhost:4502/system/console/configMgr
    then search for the MyConfig, you can see the config we created above as below:

    OSGi Configuration with the Repository (CRX Nodes) -

    To add the new configuration to the repository:
    • Go to your project folder- /apps/<yourProject>, then create the config folder (sling:Folder):
                config - applicable to all run modes
                config.<run-mode> - specific to a particular run mode
    • Under this folder create a node:
               Type: sling:OsgiConfig
               Name: the persistent identity (PID)
               for eg. in our case-  com.foo.community.core.MyConfig
    • For each parameter that you want to configure, create a property on this node:
              Name: the parameter name(shown in brackets) in the Web console;
              Type: as appropriate.
              Value: as required.

    Below is the screenshot of the config I created for global and author runmode-











    It's xml representation (com.foo.community.core.MyConfig.xml) is shown below:
    <?xml version="1.0" encoding="UTF-8"?>
    <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
        xmlns:jcr="http://www.jcp.org/jcr/1.0"
        jcr:primaryType="sling:OsgiConfig"
        sampleProperty="Value edited from crx author"/>
    
    Reading config values through Javascript Use-API:

    Use the below JS Use-API code snippet to use the config value and return to sightly-

    var global = this;
    use(function () {
        if (global.sling && global.Packages) {
           var myConfigService = global.sling.
               getService(global.Packages.com.foo.community.core.MyConfig);
        }
     var sampleProperty = myConfigService.getSampleProperty();
    
        return {
            sampleProperty : sampleProperty
        };
    });
    
    To read osgi config values through Java, refer this post.

    Resolution Order of the configurations-


    Resolution Order at Startup-

    The following order of precedence is used:
    • Repository nodes under /apps/*/config of type sling:OsgiConfig.
    • Repository nodes with type sling:OsgiConfig under /libs/*/config.
    • Any .config files from <cq-installation-dir>/crx-quickstart/launchpad/config/ on local file system.

    Resolution Order at Runtime-

    Configuration changes made while the system is running trigger a reload with the modified configuration.
    Then the following order of precedence applies:
    • Modifying a configuration in the Web console will take immediate effect as it takes precedence at runtime.
    • Modifying a configuration in /apps will take immediate effect.
    • Modifying a configuration in /libs will take immediate effect.

    Saturday, 19 May 2018

    Internationalization (I18n) in AEM using Sightly, JS and Java

    AEM enables you to internationalize strings which allows you to display localized strings in your UI.
    Internationalization (i18n) is a process of translating your content (strings) in different languages according to your requirement.
    You can internationalize strings in the following types of resources:
    • Java source files.
    • HTML using Sightly
    • Javascript in client-side libraries or in page source.
    First of all, create a i18n folder of type sling:folder under your project folder in apps, then create a json file of your language name and add jcr:mixinTypes as mix:language from tools option in crx bar and then add a property jcr:language and value as your language code as shown below:

    The content of en.json will be key-value pairs in json format for each language file. eg:
    en.json

    {
     "Hello {0} !! This is coming from {1}" : "Hello {0} !! This is coming from {1}",
     "key.java" : "This is coming from Java"
    }
    fr.json
    {
     "Hello {0} !! This is coming from {1}" : "Bonjour {0} !! cela vient de {1}",
     "key.java" : "Cela vient de Java"
    }
    The advantage of having the above json approach over creating sling:messageEntry node and then adding sling:key and sling:message property is, in a single json file you can enter as much key-value entries for your message instead of creating node each time for each message entry.

    Translator to manage Dictionaries in AEM -


    AEM provides a console for managing the various translations of texts used in component UI.
    Use the translator tool to manage English strings and their translations. The dictionaries are created in the repository, for eg. in our cas/apps/training/i18n.

    The tranlsator console is available at http://localhost:4502/libs/cq/i18n/translator.html

    Below is the screenshot of our entries in i18n folder shown in this console:


















    Using i18n in Java code -


    The com.day.cq.i18n Java package enables you to display localized strings in your UI. The I18n class provides the get method that retrieves localized strings from the AEM dictionary. To include this package, add the below dependency in your pom.xml
    <dependency>
        <groupId>com.day.cq</groupId>
        <artifactId>cq-i18n</artifactId>
        <version>5.4.0</version>
        <scope>provided</scope>
    </dependency>
    
    Below java class demonstrate the use of I18n class and how to translate content:
    package com.foo.community.core;
    
    import java.util.Locale;
    import java.util.ResourceBundle;
    import com.day.cq.i18n.I18n;
    import com.adobe.cq.sightly.WCMUsePojo;
    
    public class I18nUtility extends WCMUsePojo{
     
     private String val;
     Locale locale;
     ResourceBundle resourceBundle;
     
     @Override
     public void activate() throws Exception {
      
         locale = new Locale("en");
         if(getResourcePage().getPath().contains("fr")){
      locale = new Locale("fr"); 
         }
         resourceBundle = getRequest().getResourceBundle(locale);
         I18n i18n = new I18n(resourceBundle);
         val = i18n.get("key.java"); 
     }
     public String getVal() {
      return val;
     }
    }
    
    
    You can also get the page locale by using:
    Locale pageLang = currentPage.getLanguage(false);


    Using i18n in Sightly code -


    This is the simplest way to use i18n in aem, just you have to use the @i18n with the key name
    and you will get output according to your page locale.
    For eg.
    <p> ${"Hello {0} !! This is coming from {1}" @ i18n, 
       format=['Prasanna','Sightly'], context='html'} </p>
    
    <p data-sly-use.i18j = 'com.foo.community.core.I18nUtility'>${i18j.val}</p>
    
    As you can see from the above code, I have passed 2 parameters as an argument to dynamically generate the translation content. So whatever will be inside {} will be treated as to be replaced by dynamic arguments.

    Now I am printing the above value along with using java class and the value returned from it in sightly code. Below screenshot shows the output in English and French page-


    Using i18n in Javascript code -


    The Javascript API enables you to localize strings on the client. 
    The granite.utils client library folder provides the Javascript API. To use the API, include this client library folder on your page. Localization functions use the Granite.I18n namespace.
    Below is the example of using i18n in JS:
    Granite.I18n.setLocale("en");
    if(window.location.href.indexOf("fr")>=0){
       Granite.I18n.setLocale("fr"); 
    }
    alert(Granite.I18n.get("Hello {0} !! This is coming from {1}"
       ,['Prasanna', 'Javascript']));
    
    Below screenshot of the English and French page shows the JS alert of how to use i18n in js by passing dynamic arguments as parameter:


    Now you can pickup the method which ever you want to use and get started with the internationalization in AEM.

    References https://helpx.adobe.com/experience-manager/6-3/sites/developing/using/i18n-dev.html

    Sunday, 6 May 2018

    Maven - A simplistic start to an AEM project


    Apache Maven is an open-source project management and comprehension tool that automates builds and provides quality project information, and it’s the recommended build management tool for AEM projects.

    Maven archetype for AEM project-

    It's not possible whenever you want to create a new AEM project; you manually create the folder, files and the whole project structure, that is where the maven archetypes come in picture. Maven archetypes provide us with predefined project structure templates for AEM.

    With the help of maven archetypes, a developer can create a new project which will be in a way with the best industry practices and recommended by Adobe.

    Maven archetypes also include a sample project, which helps developer to get introduced to best practices and features employed by maven.
    Using an Archetype 10 or above project, you are given a set of files to start with.

    Maven Setup:

    • Download Maven(Binary zip file) from http://maven.apache.org/download.cgi
    • Extract the zip file to your C directory
    • Create new environment variables and enter values of java and maven path: 
               JAVA_HOME - C:\Program Files\Java\jdk1.8.0_31
               M3_HOME - C:\apache-maven-3.5.3-bin\apache-maven-3.5.
    • Edit Path environment variable and add jdk bin path(if not already added) and maven bin path:
              %M3_HOME%\bin; %JAVA_HOME%\bin
    • Test if Maven is properly configured by typing below command in command prompt (It should give you maven and java home and versions)mvn –version
    • Create .m2 folder for maven repository:
             Go to C:\Users\(your user name), create .m2 folder and create a file settings.xml (you can              copy the content of settings.xml from here ) in that folder.

    Creating a Maven archetype 12 project-

    To create an AEM archetype (version 12) project, perform these steps:
    • Open the command prompt and go to your project folder.
    • Run the following Maven command:
    mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -DarchetypeGroupId=com.adobe.granite.archetypes -DarchetypeArtifactId=aem-project-archetype -DarchetypeVersion=12 -DarchetypeCatalog=https://repo.adobe.com/nexus/content/groups/public/ 
    • When prompted, specify the following information:

    • Once done, you will see a message like:



















    • And your working directory is added with below project structure:




    Add Maven project in Eclipse:


    •        Click on File-> Import -> Maven -> Existing Maven Projects
    •        Select the project from your drive, click Finish:
































    You can see below files and packages in Eclipse, and accordingly you can add/modify files.

     After you import the project into Eclipse, notice   each module is a separate Eclipse project:
            ·  core - where Java files that are used in OSGi   services and sling servlets, sling models are located
            ·  launcher - where additional Java files are located
            ·  tests - Java files for tests like JUNIT tests
            ·  apps - content under /apps
            ·  content - content under /content
    When you want to create an OSGi service, you work under core. Likewise, if you want to create a HTL component, you can work under apps.

     








    Code Deployment in AEM using maven:


    To build the OSGi bundle by using Maven, perform these steps:

    ·         Open the command prompt and go to your project folder.
    ·         Run the following maven command: mvn -PautoInstallPackage install.
    ·    After the build success, the OSGi bundle can be found in the following folder: (you folder path)\MyProject\core\target. The name of the OSGi bundle is MyProject.core-1.0-SNAPSHOT.jar.
    The above command automatically deploys the OSGi bundle and apps package to AEM.
    ·   Make sure after you deploy code, your bundle is in active state, you can check it at http://localhost:4502/system/console/bundles



          ·  Also, you can see the following components and items installed in your crxde along with a sample page inside content named MyProject.
          











    Dependency Mechanism in Maven

    Dependency management is one of the features of Maven that is best known to users and is one of the areas where Maven excels. There is not much difficulty in managing dependencies for a single a project, but when you start getting into dealing with multi-module projects and applications that consist of tens or hundreds of modules this is where Maven can help you a great deal in maintaining a high degree of control and stability.

    When you have a set of projects that inherits a common parent it's possible to put all information about the dependency in the common POM and have simpler references to the artifacts in the child POMs.
    For ex: you can add the below dependency in the parent POM of the project like this:
    <dependency>
              <groupId>com.adobe.aem</groupId>
              <artifactId>uber-jar</artifactId>
              <version>6.3.0</version>
              <classifier>apis</classifier>
              <scope>provided</scope>
    </dependency>
    And have its reference in the core POM.xml as below:
    <dependency>
             <groupId>com.adobe.aem</groupId>
             <artifactId>uber-jar</artifactId>
             <classifier>apis</classifier>
    </dependency>

    The above JAR file (uber-jar) contains all of the public Java APIs exposed by Adobe Experience Manager. It includes limited external libraries as well, specifically all public APIs available in AEM. 
    The minimal set of information for matching a dependency reference against a dependency Management section is actually {groupId, artifactId, type, classifier}. But we can shorthand the identity set to {groupId, artifactId}, since the default for the type field is jar, and the default classifier is null.
    So, now you are good to go!! Just create your project and start developing.