AEM 6 SP2 - Change Parsys Border and Text Color in Classic UI

Goal


Change the color of parsys. In Classic UI, the parsys border color is #d3ea9a; if your website background happens to be of same color or site CSS cannot contrast CQ parsys, the following simple css override might help

For Touch UI check this post

For a better implementation check this post

Package Install


Product Parsys




Extension Parsys



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-change-parsys-color

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-change-parsys-color/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-change-parsys-color/clientlib/css.txt, add the following

                         highlight.css

4) Create file ( type nt:file ) /apps/classic-ui-change-parsys-color/clientlib/highlight.css, add the following styles

#CQ .cq-editrollover-highlight-left {
     background-color: red !important;
}

#CQ .cq-editrollover-highlight-bottom {
    background-color: red !important;
}

#CQ .cq-editrollover-highlight-top {
    background-color: red !important;
}

#CQ .cq-editrollover-highlight-right {
    background-color: red !important;
}

#CQ .cq-editrollover-insert-message {
    color: red !important;
}


AEM 6 SP 2- Classic UI Create Page and Open in Scaffolding View

Goal


Extend the Classic UI create page dialog to create & open in scaffolding view. A similar post on extending the dialog to provide create & view is here

Demo | Package Install




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/create-page-open-scaffolding-view

2) Create clientlib (type cq:ClientLibraryFolder/apps/create-page-open-scaffolding-view/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/create-page-open-scaffolding-view/clientlib/js.txt, add the following

                         open.js

4) Create file ( type nt:file ) /apps/create-page-open-scaffolding-view/clientlib/open.js, add the following code

(function(){
    var cqCreatePageDialog = CQ.wcm.Page.getCreatePageDialog;

    CQ.wcm.Page.getCreatePageDialog = function(parentPath){
        var dialog = cqCreatePageDialog(parentPath);

        var panel = dialog.findBy(function(comp){
            return comp["jcr:primaryType"] == "cq:Panel";
        }, dialog);

        if(!panel || panel.length == 0){
            return;
        }

        dialog.buttons.splice(0,0,new CQ.Ext.Button( {
                text: "Scaffolding View",
                width: 140,
                tooltip: 'Create page and open in scaffolding view',
                handler: function(button){
                    dialog.ok(button, function(form, resp){
                        try{
                            var text = resp.response.responseText;
                            var loc = text.substring(text.indexOf("\"", text.indexOf("href=")) + 1);

                            loc = "/cf#" + loc.substr(0, loc.indexOf("\"")) + ".scaffolding.html";
                            window.location = loc;
                        }catch(err){
                            console.log("page create and scaffolding view - error parsing html response");
                        }
                    });
                }}
        ));

        return dialog;
    }
})();

AEM 6 SP2 - Show Confirm Box before Node Move in CRXDE Lite

This is an unconventional way of extending CRXDE Lite; for desperate situations only

Goal


This post is on showing confirm prompt when user attempts node move in CRXDE Lite (http://localhost:4502/crx/de) or use shift key + move for moving a node

Ext.Msg.confirm works asynchronously, so if a confirm box is absolutely needed use browser confirm

Demo | Package Install


Confirm Prompt




No Shift Press while Drag



Solution


Follow the two steps below to extend CRXDE Lite and add necessary JS to show confirm. First step is Not Upgrade-Proof, so when CQ is upgraded, the first step may have to be repeated

Step 1 - Update CRXDE Lite Jar

All we do in this step is copy (back it up just in case if something goes wrong) the serialized CRXDE lite jar, open it and add a small chunk of JS code so that any extensions we code are loaded by the added JS logic when lite is opened in browser.

1) Access bundles console http://localhost:4502/system/console/bundles and find the CRXDE Support bundle





2) Search for the serialized bundle on filesystem and copy it to a temp location (take a backup before modifying). On my AEM 6 SP2 its available in author\crx-quickstart\launchpad\installer (rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser)

3) Rename the copied .ser file to .jar (eg. rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser -> rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar)

4) Open the jar using zip executable (say winrar), open file docroot\js\start.js in any text editor and add following code at the end. Save file and a winrar confirmation should popup asking if the jar should be updated with saved file.

Ext.onReady(function() {
    var loadLiteExtns = function(){
        Ext.Ajax.request({
            url: "/apps/ext-crxde-lite/files.txt",
            success: function(response, options) {
                var js = response.responseText;
 
                if(!js){
                    return;
                }
 
                js = js.split("\n");
 
                Ext.each(js, function(jsPath) {
                    Ext.Ajax.request({
                        url: jsPath,
                        success: function(response, options) {
                            eval(response.responseText);
                        }
                    });
                });
            }
        });
    };
 
    loadLiteExtns();
});


5) In the above steps we added necessary code to load the extension files entered in /apps/ext-crxde-lite/files.txt. So whenever a new CRXDE Lite extension is needed a new line with extension file path can be added in /apps/ext-crxde-lite/files.txt

6) Access http://localhost:4502/system/console/bundles, click Install/Update... to upload and update CQ with the new CRXDE Support jar having necessary code to load the CRXDE Lite extension files.

Step 2 - Add extension files in CRX

In this step we add the JS file containing logic to show confirm

1) Access http://localhost:4502/crx/de

2) Create node /apps/ext-crxde-lite of type nt:folder

3) Create node /apps/ext-crxde-lite/files.txt of type nt:file and add the following line. The logic added in Step 1 reads this file for loading JS extension files added as paths

                                 /apps/crxde-move-node-confirm/move-confirm.js

4) Create node /apps/crxde-move-node-confirm/move-confirm.js of type nt:file and add the following code

Ext.onReady(function(){
    var INTERVAL = setInterval(function(){
        var tree = Ext.getCmp(CRX.ide.TREE_ID);

        if(!tree){
            return;
        }

        clearInterval(INTERVAL);

        var listeners = tree.initialConfig.listeners;

        tree.removeListener("beforenodedrop", listeners.beforenodedrop, tree);

        tree.on("beforenodedrop", function(dropEvent){
            /*var r = confirm("You are trying to move a node, Are you sure?");

            if (r) {
                return listeners.beforenodedrop.call(tree, dropEvent);
            } else {
                return false;
            }*/

            //uncomment this block for stopping node move, with no shift key press
            var shiftKey = dropEvent.rawEvent.browserEvent.shiftKey;

            if(!shiftKey){
                Ext.Msg.alert("Alert", "If you'd like to move a node, press shift on keyboard before dragging");
                return false;
            }

            return listeners.beforenodedrop.call(tree, dropEvent);
        });
    }, 250);
});



AEM 6 SP2 - Classic UI Side By Side Version Diff Compare

Goal


Create a mirror view or side by side compare of page version diff and current version so that user can spot changes and view current version at the same time in single window

The demo uses iframes scaled down do 80%, to show version diff and current version in two panes with pages loaded as wcmmode disabled; user is not logged into aem in the iframes, so page shows anonymous view

To keep view in sync, the two iframes listen to scroll events of each other; if view sync is not required, comment evenScroll() calls

When the content finder is open, diff extension window some times doesn't show on top of side kick and content finder (at the front) as shown here, a known bug

Demo | Package Install


Side by Side Diff button




Side by Side Compare window




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classic-ui-side-by-side-diff

2) Create node /apps/classic-ui-side-by-side-diff/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets and dependencies of type String[] with value underscore

3) Create file (nt:file) /apps/classic-ui-side-by-side-diff/clientlib/js.txt and add

                       side-by-side-diff.js

4) Add the following code in /apps/classic-ui-side-by-side-diff/clientlib/side-by-side-diff.js


CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.SideDiff = {
    LEFT_PANE: 'eaem-left-pane',
    RIGHT_PANE: 'eaem-right-pane',
    BUT_ID: 'eaem-side-diff',

    createDiffWindow: function(lUrl, rUrl){
        var E = ExperienceAEM.SideDiff;

        //scale down the iframe to 80% for better view
        var frameStyle= "style='width: 1000px; height: 1000px; " +
                        "-webkit-transform: scale(0.8); -webkit-transform-origin: 0 0; " +
                        "-moz-transform: scale(0.8); -moz-transform-origin: 0px 0px;'";

        var lFrame = "<iframe " + frameStyle + " id='" + E.LEFT_PANE + "' src='" + lUrl + "'></iframe>";
        var rFrame = "<iframe " + frameStyle + " id='" + E.RIGHT_PANE + "' src='" + rUrl + "'></iframe>";

        var divStyle = "width: 800px; height: 1000px; ";

        var lBox = new CQ.Ext.BoxComponent({
            autoEl: { tag: 'div', html: lFrame, style: divStyle }
        });

        var rBox = new CQ.Ext.BoxComponent({
            autoEl: { tag: 'div', html: rFrame, style: divStyle }
        });

        var panel = new CQ.Ext.Panel({
            header: false,
            border: false,
            layout:'fit',
            height: 820,
            width: 1600,
            items:[{
                layout:'table',
                layoutConfig: {
                    columns: 2
                },
                items:[lBox , rBox ]
            }]
        });

        var config = {
            title: "Side by Side Compare",
            x: 25,
            y:37,
            items: [ panel ]
        };

        var win = new CQ.Ext.Window(config);

        win.on('show', function(){
            var $left = $("#" + E.LEFT_PANE), $right = $("#" + E.RIGHT_PANE);

            //to keep view in sync, the two iframes listen to scroll events of each other
            evenScroll($left, $right);
            evenScroll($right, $left);

            win.toFront(true);
        });

        function evenScroll($one, $two){
            $one.load(function(){
                $($one[0].contentWindow.document).scroll(function(){
                    var $this = $(this);
                    $two[0].contentWindow.scrollTo($this.scrollLeft(),$this.scrollTop());
                });
            });
        }

        win.show();
    },

    addButton: function(){
        var E = ExperienceAEM.SideDiff;

        //buttons panel is the first component of "Restore Version" panel
        var bPanel = this.getComponent(0);
        var sideDiff = bPanel.getComponent(E.BUT_ID);

        //check if side diff button was already added
        if( sideDiff != null){
            return;
        }

        var sk = CQ.WCM.getSidekick();

        //get the grid containing versions.. grid panel is the second component of "Restore Version" panel
        //grid is the only component in grid panel
        var grid = this.getComponent(1).getComponent(0);

        sideDiff = {
            xtype: "button",
            id: E.BUT_ID,
            text: "| Diff |",
            handler: function() {
                var rec = grid.getSelectionModel().getSelected();

                if (!rec) {
                    CQ.Ext.Msg.alert("Select", "Please select a version");
                    return;
                }

                var HTTP = CQ.HTTP;

                var left = HTTP.externalize(sk.getPath() + ".html");
                var right = HTTP.externalize(sk.getPath() + ".html");

                left = HTTP.addParameter(left, "cq_diffTo", rec.data.label);
                left = HTTP.addParameter(left, "wcmmode", "disabled");
                left = HTTP.noCaching(left);

                right = HTTP.addParameter(right, "wcmmode", "disabled");
                right = HTTP.noCaching(right);

                E.createDiffWindow(left, right);
            }
        };

        bPanel.add(sideDiff);

        this.doLayout();
    },

    getPanel: function(){
        var sk = CQ.WCM.getSidekick();

        if(!sk){
            return null;
        }

        var rVersion = null;

        try{
            var vPanel = sk.panels[CQ.wcm.Sidekick.VERSIONING];

            if(!vPanel){
                return null;
            }

            rVersion = vPanel.findBy(function(comp){
                return comp["title"] == "Restore Version";
            });

            if(_.isEmpty(rVersion)){
                return null;
            }
        }catch(err){
            console.log("Error adding side by side diff", err);
        }

        return rVersion[0];
    }
};

(function(){
    var pathName = window.location.pathname;

    if( ( pathName !== "/cf" ) && ( pathName.indexOf("/content") !== 0)){
        return;
    }

    var E = ExperienceAEM.SideDiff;

    var SK_INTERVAL = setInterval(function(){
        //get the Restore Version panel
        var panel = E.getPanel();

        if(!panel){
            return;
        }

        clearInterval(SK_INTERVAL);

        panel.on('activate', E.addButton);
    }, 250);
})();




AEM 6 SP2 - Simple Dispatcher Configuration (Author -> Publish -> Dispatcher)

Goal


Create simple Author -> Publish -> Dispatcher configuration on developer's box (not production). Assuming Author aem is running on port 4502 and Publish aem on port 4503, this post is on adding & configuring dispatcher module on Windows Apache 2.2 Http Server running on port 80. For product documentation on configuring dispatcher check this page and this page


Author - Create Page

1) Start by creating a template on author or download this sample package (contains template Basic Template); installing it creates the following structure (/apps/samples) in CRX (http://localhost:4502/crx/de)



2) Create a page of type Basic Template and add a text component (for information on creating CQ template components check this post). Here is the page created - http://localhost:4502/editor.html/content/experience-aem/english.html



3) The page node structure in author CRX




Publish - View Page

1) Try to view the page on publish, created on author above. As the page isn't published yet, accessing publish http://localhost:4503/content/experience-aem/english.html results in 404

2) Go back to author for publishing the page. Before page is published, package containing the template component needs to be replicated. Access CRXDE package manager of author (http://localhost:4502/crx/packmgr/index.jsp) and replicate the package basictemplate.zip




3) When replication is successful, the necessary /apps/samples node structure should have been created in Publish CRX (http://localhost:4503/crx/de/index.jsp#/apps/samples)

4) Publish the page on author. The node /content/experience-aem/english gets created in publish crx confirming page publish



5) Access the published page on http://localhost:4503/content/experience-aem/english.html


6) User now successfully authored and published  page /content/experience-aem/english.html


Dispatcher - Cache page

1) Using a Web Server and CQ Dispatcher to cache published pages, brings great performance by serving static html and limiting requests sent to publish server for recreating the same pages over and over for each client request

2) Assuming Apache 2.2 Http Server installed, download the dispatcher module. Dispatcher releases are independent of AEM releases, so find the right dispatcher for your OS (Windows), Web Server (Apache) and not AEM - dispatcher-apache2.2-windows-x86-4.1.9.zip (if this dispatcher version was updated, link may not work, download it from the page)

3) Unzip the file dispatcher-apache2.2-windows-x86-4.1.9.zip and copy disp_apache2.2.dll to apache modules folder (eg. C:\dev\code\install\Apache2.2\modules)

4) Open apache httpd.conf file (eg. C:\dev\code\install\Apache2.2\conf\httpd.conf) and add the following line to include dispatcher configuration (added in next steps)

Include conf/dispatcher.conf




4) Create file dispatcher.conf in conf folder (eg. C:\dev\code\install\Apache2.2\conf\dispatcher.conf) and add the following configuration. For more details on available parameters like DocumentRoot check this adobe doc and apache http server documentation

LoadModule dispatcher_module modules\disp_apache2.2.dll

<IfModule disp_apache2.c>
  DispatcherConfig conf/dispatcher.any
  DispatcherLog    logs/dispatcher.log
  DispatcherLogLevel 3
  DispatcherNoServerHeader 0
  DispatcherDeclineRoot 0
  DispatcherUseProcessedURL 0
  DispatcherPassError 0
</IfModule>

<Directory />
  <IfModule disp_apache2.c>
    SetHandler dispatcher-handler
    ModMimeUsePathInfo On
  </IfModule>

  Options FollowSymLinks
  AllowOverride None
</Directory>

5) #1 loads the dispatcher module, #4 specifies the dispatcher rules file (created in next step)

6) Create file dispatcher.any in conf folder (eg. C:\dev\code\install\Apache2.2\conf\dispatcher.any) with the following configuration

/farms {
 /experience-aem {   
  /clientheaders {
   "*"
  }
  /virtualhosts {
   "*"
  }
  /renders {
   /rend01 {
    /hostname "127.0.0.1"
    /port "4503"
   }
  }   
  /filter {
   /0001 { 
    /type "deny" /glob "*" 
   }    
   /0002 { 
    /type "allow" /url "/content*" 
   }
   /0003 { 
    /type "allow" /url "/etc/designs*" 
   }
   /0004 { 
    /type "allow" /url "/etc/clientlibs*" 
   }
  }
  /cache {
   /docroot "C:/dev/code/install/Apache2.2/dispatcher/cache"
   /rules {
    /0000 {
     /glob "*"
     /type "allow"
    }
   }
  }  
 }
}

7) #2 is name of farm which could be any, #10 render is the publish instance serving pages to be cached (1:1 dispatcher to publish mapping recommended). #17 first denies access to every resource and making exceptions at #20 to allow resources under /content, #23 for allowing site css, #26 for js files, minimally needed for typical websites. #30 specifies the cache folder where static content generated from publish servers is cached for any future requests until any cached content is invalidated (next sections). Rule at #32 directs dispatcher to cache all content. For more indepth dispatcher configuration check adobe documentation

8) With the above configuration in place, restart apache server, open a new browser instance and access the sample page on dispatcher running on default port 80 - http://localhost/content/experience-aem/english.html

9) The following statements should haven be logged in logs/dispatcher.log (eg. C:\dev\code\install\Apache2.2\logs\dispatcher.log) and cached content available in dispatcher/cache folder (eg. C:\dev\code\install\Apache2.2\dispatcher\cache)





Dispatcher - why is content not cached?

1) If the page requested from dispatcher (http://localhost/content/experience-aem/english.html) returns a 200 and available, but not cached in dispatcher; the reason could be login-token cookie present in browser. So you are logged into AEM in one tab and try to access the page on dispatcher in another tab; an AEM session is available and the following statement is logged in dispatcher.log



2) The statement above request contains authorization is logged when there is a login-token cookie sent with request (in this configuration, author AEM and dispatcher both have domain localhost). Using any REST client (chrome extension POSTMAN) to view the request headers and cookies...

3) Try accessing http://localhost/content/experience-aem/english.html by logging out of AEM or opening a new browser instance or deleting the login-token cookie from browser or configure /allowAuthorized in dispatcher.any (for more information on caching requests containing authentication information check this adobe documentation) and the page html should be cached

           Request sent with no login-token cookie (firefox with no AEM session)


           Log statement for create cache



           Cached file on file system


   

Dispatcher - Cache Invalidation 

1) Invalidating dispatcher cache is required to make sure it doesn't serve stale content. Auto invalidation or sending cache invalidate requests - /dispatcher/invalidate.cache are ways to delete  (invalidate) dispatcher cache

2) For publish instance to send dispatcher invalidate cache requests when there is content update on a page, the Dispatcher Flush Agent needs to be configured

3) Access http://localhost:4503/etc/replication/agents.publish.html, click on Dispatcher Flush (flush) agent -> Settings Edit and enable it (if you are not logged into publish instance, a 404 is returned; login to publish CRX http://localhost:4503/crx/de)




4) Enter the invalidate cache url of dispatcher running on port 80 - http://localhost:80/dispatcher/invalidate.cache




5) Here is the dispatcher flush agent on publish enabled





6) Publish is now configured to send invalidate cache requests to dispatcher; lets modify the page on author and publish it





7) When the page gets replicated (published) from author to publish the following statements should be logged in publish\crx-quickstart\logs\error.log confirming the page flush from dispatcher cache


8) Dispatcher flush request received and logged, in dispatcher.log as shown in picture below. The page should have been deleted from file system (eg. from C:\dev\code\install\Apache2.2\dispatcher\cache\content\experience-aem)



9) Access the page again to see updated content (and cache it)



10) A manual cache invalidation request can also be sent using any rest client (or curl) by adding the following custom headers. To send a cache invalidation request on page /content/experience-aem/english.html, add the following headers to request url http://localhost:80/dispatcher/invalidate.cache

                CQ-Action: Activate
                CQ-Handle: /content/experience-aem/english
                CQ-Path: /content/experience-aem/english

11) Dispatcher should be protected from unauthorized cache invalidate requests (hackers). On production systems /allowedClients should be configured in dipsatcher.any with the publish server ips sending cache invalidate requests, check adobe documentation



AEM 6 SP2 - View (Tail) CQ Log Files

Goal


Quick post on creating a Tail Log servlet for examining the logs on remote CQ instances; if you are debugging issues on a friend's CQ instance or QA, deploying this servlet could be helpful. Ootb, logs can be viewed by logging into felix console (http://localhost:4502/system/console/status-slinglogs) or doing a remote login (ssh) or http://localhost:4502/bin/crxde/logs?tail=1000

Demo | Package Install | Source Code

View error log : http://localhost:4502/bin/experience-aem/tail/log

View specific log (eg. access.log) : http://localhost:4502/bin/experience-aem/tail/log?log=access

Clear: Clears the current log view

Color Line Begin - First few characters of the line are colored (next fetch)

Start Line At - Cut out the first few characters of line, say timestamp (next fetch)

Line Min Length - Only the lines with length greater than entered number are returned (next fetch)

Line Max Length - Only the lines with length less than entered number are returned (next fetch)

Trim to Size - Cut the line to specified size (next fetch)

If Line Contains - Get line only if it contains the entered string (next fetch)

Not If Line Contains - Get line only if it does not contain entered string (next fetch)


Solution


Create an OSGI servlet apps.experienceaem.taillogs.TailLogsServlet with the following code

package apps.experienceaem.taillogs;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.*;
import java.util.Dictionary;
import org.apache.commons.lang3.StringEscapeUtils;

@Component(
        metatype = true,
        label = "Experience AEM Tail Logs",
        description = "Experience AEM Tail Logs Servlet")
@Service
@Properties({
        @Property(name = "sling.servlet.methods", value = {"GET", "POST"}, propertyPrivate = true),
        @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/tail/log", propertyPrivate = true)})
public class TailLogsServlet extends SlingAllMethodsServlet {
    private final Logger LOG = LoggerFactory.getLogger(getClass());

    @Reference
    protected SlingSettingsService slingSettings;

    @Property(name = "logs.path", label = "Logs folder location",
            value = "",
            description = "Absolute path of log files")
    private static final String LOG_PATH = "log.path";

    @Property(name = "bytes.to.read", label = "Initial Bytes to Read",
            value = "2048",
            description = "The initial bytes to read")
    private static final String DEFAULT_BYTES_TO_READ = "bytes.to.read";

    @Property(name = "refresh.interval.millis", label = "Refresh Interval",
            value = "5000",
            description = "Log textarea refresh interval in millis")
    private static final String REFRESH_INTERVAL = "refresh.interval.millis";

    private String logFolderPath = null;
    private String bytesToRead = null;
    private String refreshInterval = null;

    @Activate
    protected void activate(final ComponentContext context) {
        Dictionary<String, Object> props = context.getProperties();
        Object prop = props.get(LOG_PATH);

        if(prop == null){
            logFolderPath = slingSettings.getSlingHomePath() + File.separator + "logs" + File.separator;
        }else{
            logFolderPath = String.valueOf(prop);
        }

        bytesToRead = String.valueOf(props.get(DEFAULT_BYTES_TO_READ));
        refreshInterval = String.valueOf(props.get(REFRESH_INTERVAL));

        LOG.info("Logs path : " + logFolderPath + ", Initial bytes to read : " + bytesToRead + ", Refresh interval : " + refreshInterval);
    }

    private void addLastNBytes(SlingHttpServletRequest request, SlingHttpServletResponse response, String logName)
                                    throws Exception{
        String filePointer = request.getParameter("pointer");
        String startLineAt = request.getParameter("startLineAt");
        String lineMinLength = request.getParameter("lineMinLength");
        String lineMaxLength = request.getParameter("lineMaxLength");
        String lineContains = request.getParameter("lineContains");
        String notLineContains = request.getParameter("notLineContains");
        String colorLineBegin = request.getParameter("colorLineBegin");
        String trimToSize = request.getParameter("trimToSize");

        File file = new File(logFolderPath + logName);

        long _filePointer = -1, len;
        int btr = Integer.parseInt(this.bytesToRead);

        if(StringUtils.isEmpty(filePointer)){
            _filePointer = file.length() - btr;
        }else{
            _filePointer = Long.parseLong(filePointer);

            len = file.length();

            //roll over or log clean
            if( len < _filePointer){
                _filePointer = len - btr;
            }
        }

        if(_filePointer < 0){
            _filePointer = 0;
        }

        StringBuilder sb = new StringBuilder();

        //based on //http://www.jibble.org/jlogtailer.php
        RandomAccessFile raf = new RandomAccessFile(file, "r");

        try{
            raf.seek(_filePointer);

            String line = null; int startAt = 0;

            if(StringUtils.isNotEmpty(startLineAt)){
                startAt = Integer.parseInt(startLineAt);
            }

            while ((line = raf.readLine()) != null) {
                if(startAt > 0 ){
                    if(line.length() > startAt){
                        line = line.substring(startAt);
                    }else{
                        continue; //skip lines shorter than desired
                    }
                }

                if(StringUtils.isNotEmpty(lineMinLength) && line.length() < Integer.parseInt(lineMinLength)){
                    continue;
                }

                if(StringUtils.isNotEmpty(lineMaxLength) && line.length() > Integer.parseInt(lineMaxLength)){
                    continue;
                }

                if(StringUtils.isNotEmpty(lineContains) && !line.contains(lineContains)){
                    continue;
                }

                if(StringUtils.isNotEmpty(notLineContains) && line.contains(notLineContains)){
                    continue;
                }

                if(StringUtils.isNotEmpty(trimToSize)){
                    line = line.substring(0, Integer.parseInt(trimToSize));
                }

                if(StringUtils.isNotEmpty(colorLineBegin) && colorLineBegin.equals("true")){
                    int length = (line.length() < 10) ? 1 : 10;
                    line = "<span style='color:red;font-weight:bold'>" + StringEscapeUtils.escapeHtml4(line.substring(0,length))
                            + "</span>" + StringEscapeUtils.escapeHtml4(line.substring(length));
                }

                sb.append(line);
            }

            _filePointer = raf.getFilePointer();

            raf.close();
        }catch(Exception e){
            raf.close();
            throw new ServletException("Error reading file - " + logName);
        }

        PrintWriter pw = response.getWriter();

        if(StringUtils.isEmpty(filePointer)){
            response.setContentType("text/html");

            String interval = Integer.parseInt(this.refreshInterval)/1000 + " secs";

            pw.write("<html><head><title>CQ Tail Log</title><script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.js'></script><style>input[type='text']{width:50px}</style></head><body><div style='border: 1px solid; padding: 5px; height: 780px; overflow: scroll;'><code contenteditable='true' id='logData'>" + sb.toString() + "</code></div>");
            pw.write("<div>Log file : " + (logFolderPath + logName) + "</div>");
            pw.write("<div>Refreshing : <span id=status style='color:red'>" + interval + "</span> ");
            pw.write("| <input type=button value='pause' onclick=\"eaemTL.paused = !eaemTL.paused; this.value=eaemTL.paused ? 'resume' : 'pause'; $('#status').html(eaemTL.paused ? 'paused' : eaemTL.interval)\"/> ");
            pw.write("| <input type=button value='clear' onclick=\"$('#logData').html('');\"/> ");
            pw.write("| Color Line Begin : <input type=checkbox onchange='eaemTL.colorLineBegin=this.checked; updateTextArea()'/> ");
            pw.write("| Font Size : <input type=text onchange=\"$('#logData').css('font-size', this.value)\"> Px ");
            pw.write("| Start Line At : <input type=text onchange='eaemTL.startLineAt=this.value; updateTextArea()'> ");
            pw.write("| Line Min Length : <input type=text onchange='eaemTL.lineMinLength=this.value; updateTextArea()'> ");
            pw.write("| Line Max Length : <input type=text onchange='eaemTL.lineMaxLength=this.value; updateTextArea()'> ");
            pw.write("| Trim to Size : <input type=text onchange='eaemTL.trimToSize=this.value; updateTextArea()'> </div>");
            pw.write("<div>If Line Contains : <input type=text onchange='eaemTL.lineContains=this.value; updateTextArea()' style='width:600px'>      ");
            pw.write(" |     Not If Line Contains : <input type=text onchange='eaemTL.notLineContains=this.value; updateTextArea()' style='width:600px'></div> ");
            pw.write("<script type='text/javascript'>var eaemTL = { log: '" + logName + "', pointer : " + _filePointer + ", paused : false, interval : '" + interval + "' }; var $logData = $('#logData');");
            pw.write("function updateTextArea() { if(eaemTL.paused){return;} $.ajax( { url: '/bin/experience-aem/tail/log', data: eaemTL } ).done(function(data){ if(data.log){$logData.html($logData.html() + data.log)}; eaemTL.pointer = data.pointer});} setInterval(updateTextArea, 5000);</script></body></html>");
        }else{
            response.setContentType("application/json");

            JSONObject json = new JSONObject();
            json.put("log", sb.toString());
            json.put("pointer", _filePointer);

            json.write(pw);
        }
    }

    /**
     * @param request
     * @param response
     * @throws javax.servlet.ServletException
     * @throws java.io.IOException
     */
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
                        throws ServletException, IOException {
        try{
            String logName = request.getParameter("log");

            if(StringUtils.isEmpty(logName)){
                logName = "error";
            }

            if(!logName.endsWith(".log")){
                logName = logName + ".log";
            }

            addLastNBytes(request, response, logName);
        }catch(Exception e){
            LOG.warn("Error tailing logs servlet", e);
        }
    }
}

AEM 6 SP2 - Touch UI Site Admin Console Default to List View

Goal


When a user accesses Sites Admin console first time (http://localhost:4502/sites.html) the results are shown as cards by default (user can then select choice of view, stored in cookie cq.sites.childpages.layoutId for any future requests). This post is on setting the default view to List

Demo | Package Install

               List View



Solution


1) Create overlay of /libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout in /apps. Use overlay creator helper servlet for creating the structure in /apps

2) Add a property layout on /apps/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout with the following value (when cookie cq.sites.childpages.layoutId is null, select view type as list)

${state["cq.sites.childpages.layoutId"].string == null ? "list" : state["cq.sites.childpages.layoutId"].string}

An other way to select default view is by reordering the view nodes under /libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout/layouts in /apps overlay



AEM 6 SP2 - Servlet for overlaying libs path in apps

Goal


Sample servlet for creating overlay of a /libs path in /apps. Overlays can be used for overriding/extending otb implementation. It's no exaggeration almost all CQ projects may have used overlays at some point for extending product functionality. This servlet checks for type of each node under /libs (of given path) and creates similar path in /apps

Package Install | Package Install [AEM 65] | Github

A sample request - http://localhost:4502/bin/experience-aem/create/overlay?path=/libs/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout creates /apps/wcm/core/content/sites/jcr:content/body/content/content/items/childpages/layout

Add copyChildren=true to the request URL to copy (not create) the last node from libs

Solution


Create an OSGI servlet with the following code

package apps.experienceaem;

import com.day.cq.commons.jcr.JcrUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
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.apache.sling.commons.json.JSONObject;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(metatype = true, label = "Experience AEM Overlay Creator")
@Service
@Properties({
        @Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
        @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/create/overlay", propertyPrivate = true),
        @Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)})
public class OverlayPathCreatorServlet extends SlingAllMethodsServlet {
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
                                    throws ServletException,IOException {
        String path = request.getParameter("path");
        String copyChildren = request.getParameter("copyChildren");

        String srcHierarchy = "/libs", desHierarchy = "/apps";

        JSONObject json = new JSONObject();

        try {
            ResourceResolver resolver = request.getResourceResolver();

            if (StringUtils.isEmpty(path) || !path.trim().startsWith("/libs")) {
                json.put("error", "Path should start with /libs");
                response.getWriter().print(json);
                return;
            }

            Session session = resolver.adaptTo(Session.class);
            Resource destResource = null, srcResource = null, parentDestResource = null;

            Node srcNode = null; String token;
            String tokens[] = path.substring("/libs".length()).split("/");

            for(int index = 0; index < tokens.length; index++){
                token = tokens[index];

                if(StringUtils.isEmpty(token)){
                    continue;
                }

                desHierarchy = desHierarchy + "/" + token;
                srcHierarchy = srcHierarchy + "/" + token;

                destResource = resolver.getResource(desHierarchy);

                if(destResource != null){
                    continue;
                }

                srcResource = resolver.getResource(srcHierarchy);

                if(srcResource == null){
                    throw new ServletException("Error finding resource - " + srcHierarchy);
                }

                srcNode = srcResource.adaptTo(Node.class);

                if(index == (tokens.length - 1) && "true".equalsIgnoreCase(copyChildren)){
                    JcrUtil.copy(srcNode, parentDestResource.adaptTo(Node.class), null);
                }else{
                    JcrUtil.createPath(desHierarchy, srcNode.getPrimaryNodeType().getName(), session);
                    parentDestResource = resolver.getResource(desHierarchy);
                }
            }

            session.save();

            json.put("success", "Created " + desHierarchy);

            response.getWriter().print(json);
        } catch (Exception e) {
            throw new ServletException("Error creating - " + desHierarchy, e);
        }
    }
}


AEM 6 SP2 - Touch UI Extend Site Admin Page Properties Show Submit Confirmation

Goal


Extend Site Admin console Page Properties, add submit event listener to show confirmation for page title property ( sample save button listener )

Demo | Package Install

A similar listener on dialog (cq:dialog) save is here

http://localhost:4502/libs/wcm/core/content/sites/properties.html?item=/content/geometrixx/en/products



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-extend-site-page-properties

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-extend-site-page-properties/clientlib and set a property categories of String type to cq.siteadmin.admin.properties

3) Create file ( type nt:file ) /apps/touch-ui-extend-site-page-properties/clientlib/js.txt, add the following

                         missing-page-title.js

4) Create file ( type nt:file ) /apps/touch-ui-extend-site-page-properties/clientlib/missing-page-title.js, add the following code

(function($document, $) {
    "use strict";

    //form id defined in /libs/wcm/core/content/sites/properties/jcr:content/body/content/content
    var PROPERTIES_FORM = "propertiesform";

    $document.on("foundation-contentloaded", function(){
        $(".foundation-content-current").on('click', "button[type='submit'][form='" + PROPERTIES_FORM + "']", function(e){
            var $propertiesForm = $("#" + PROPERTIES_FORM);

            if($propertiesForm.length == 0){
                return;
            }

            e.preventDefault();
            e.stopPropagation();

            var title = $propertiesForm.find("[name='./pageTitle']").val(),
                message, warning = false;

            var fui = $(window).adaptTo("foundation-ui");

            if(!title){
                message = "Page title is empty. Are you sure?";
                warning = true;
            }else{
                message = "Page title is '" + title + "'. Submit?";
            }

            fui.prompt("Confirm", message, "notice",
                [{
                    id: "CANCEL",
                    text: "CANCEL",
                    className: "coral-Button"
                },{
                    id: "SUBMIT",
                    text: "SUBMIT",
                    warning: warning,
                    primary: !warning
                }
                ],function (actionId) {
                    if (actionId === "SUBMIT") {
                        $propertiesForm.submit();
                    }
                }
            );
        });
    });
})($(document), Granite.$);

AEM 6 SP2 - Disable Search Boxes in CRXDE Lite

This is an unconventional way of extending CRXDE Lite; for desperate situations only

Goal


Disable search fields in CRXDE Lite. This extension does not disable Tools -> Query. Users may want to disable repository wide search in lite to stop random performance degrading searches on CRX

Demo | Package install



Solution


Follow the two steps below to extend CRXDE Lite and add necessary JS to disable search. First step is Not Upgrade-Proof, so when CQ is upgraded, the first step may have to be repeated

Step 1 - Update CRXDE Lite Jar

All we do in this step is copy (back it up just in case if something goes wrong) the serialized CRXDE lite jar, open it and add a small chunk of JS code so that any extensions we code are loaded by the added JS logic when lite is opened in browser.

1) Access bundles console http://localhost:4502/system/console/bundles and find the CRXDE Support bundle



2) Search for the serialized bundle on filesystem and copy it to a temp location (take a backup before modifying). On my AEM 6 SP2 its available in author\crx-quickstart\launchpad\installer (rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser)

3) Rename the copied .ser file to .jar (eg. rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser -> rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar)

4) Open the jar using zip executable (say winrar), open file docroot\js\start.js in any text editor and add following code at the end. Save file and a winrar confirmation should popup asking if the jar should be updated with saved file.

Ext.onReady(function() {
    var loadLiteExtns = function(){
        Ext.Ajax.request({
            url: "/apps/ext-crxde-lite/files.txt",
            success: function(response, options) {
                var js = response.responseText;
 
                if(!js){
                    return;
                }
 
                js = js.split("\n");
 
                Ext.each(js, function(jsPath) {
                    Ext.Ajax.request({
                        url: jsPath,
                        success: function(response, options) {
                            eval(response.responseText);
                        }
                    });
                });
            }
        });
    };
 
    loadLiteExtns();
});


5) In the above steps we add necessary code to load the extension files entered in /apps/ext-crxde-lite/files.txt. So whenever a new CRXDE Lite extension is needed a new line with extension file path can be added in /apps/ext-crxde-lite/files.txt

6) Access http://localhost:4502/system/console/bundles, click Install/Update... to upload and update CQ with the new CRXDE Support jar having necessary code to load the CRXDE Lite extension files.

Step 2 - Add extension files in CRX

In this step we add the JS file containing logic to disable search fields

1) Access http://localhost:4502/crx/de

2) Create node /apps/ext-crxde-lite of type nt:folder

3) Create node /apps/ext-crxde-lite/files.txt of type nt:file and add the following line. The logic added in Step 1 reads this file for loading JS extension files added as paths

                                 /apps/ext-crxde-lite/disable-search.js

4) Create node /apps/ext-crxde-lite/disable-search.js of type nt:file and add the following code

Ext.onReady(function(){
    var INTERVAL = setInterval(function(){
        var searchField = Ext.getCmp(CRX.ide.REPO_PATH_ID);

        if(searchField){
            clearInterval(INTERVAL);

            searchField.setDisabled(true);
        }
    }, 250);

    var SB_INTERVAL = setInterval(function(){
        var homePanel = Ext.getCmp("editors");

        if(homePanel){
            clearInterval(SB_INTERVAL);

            homePanel.findByType("panel")[0].setDisabled(true);
        }
    }, 250);
});





AEM 6 SP2 - Touch UI ( Coral UI ) Nested Multifield ( Multi Multifield )

Goal


Create a Touch UI Nested multifield (or Multi multifield). The nested one is configured with two form fields, a Textfield (granite/ui/components/foundation/form/textfield), Pathbrowser  (granite/ui/components/foundation/form/pathbrowser )

Remember, it's always recommended to minimize the  number of UI extensions in projects, given a choice, a better (or simple) design of the dialog i guess would be without a nested multifield

A Composite Touch UI multifield is available here

For AEM 62 Nested Multifield storing data as Child Nodes check this post

Demo | Package Install


Nested Multifield





Component Rendering




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create a folder (nt:folder) /apps/touch-ui-nested-multi-field-panel

2) Create a component (cq:Component) /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-fieldCheck this post on how to create a CQ component

3) Add the following xml for /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/dialog created. Ideally a cq:Dialog should be configured with necessary widgets for displaying dialog in Classic UI. This post is on Touch UI so keep it simple (node required for opening the touch ui dialog created in next step)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Dialog"
    title="Multi Field"
    xtype="dialog"/>

4) Create a nt:unstructured node /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/cq:dialog with following xml (the dialog UI as 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="nt:unstructured"
    jcr:title="Multifield TouchUI Component"
    sling:resourceType="cq/gui/components/authoring/dialog"
    helpPath="en/cq/current/wcm/default_components.html#Text">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/foundation/container">
        <layout
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <fieldset
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Sample Dashboard"
                        sling:resourceType="granite/ui/components/foundation/form/fieldset">
                        <layout
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
                        <items jcr:primaryType="nt:unstructured">
                            <column
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/foundation/container">
                                <items jcr:primaryType="nt:unstructured">
                                    <dashboard
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/textfield"
                                        fieldDescription="Enter Dashboard name"
                                        fieldLabel="Dashboard name"
                                        name="./dashboard"/>
                                    <countries
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/foundation/form/multifield"
                                        class="full-width"
                                        fieldDescription="Click '+' to add a new page"
                                        fieldLabel="Countries">
                                        <field
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                            eaem-nested=""
                                            name="./countries">
                                            <layout
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                                                method="absolute"/>
                                            <items jcr:primaryType="nt:unstructured">
                                                <column
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/container">
                                                    <items jcr:primaryType="nt:unstructured">
                                                        <country
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                            fieldDescription="Name of Country"
                                                            fieldLabel="Country Name"
                                                            name="./country"/>
                                                        <states
                                                            jcr:primaryType="nt:unstructured"
                                                            sling:resourceType="granite/ui/components/foundation/form/multifield"
                                                            class="full-width"
                                                            fieldDescription="Click '+' to add a new page"
                                                            fieldLabel="States">
                                                            <field
                                                                jcr:primaryType="nt:unstructured"
                                                                sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                                                name="./states">
                                                                <layout
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
                                                                    method="absolute"/>
                                                                <items jcr:primaryType="nt:unstructured">
                                                                    <column
                                                                        jcr:primaryType="nt:unstructured"
                                                                        sling:resourceType="granite/ui/components/foundation/container">
                                                                        <items jcr:primaryType="nt:unstructured">
                                                                            <state
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                                                fieldDescription="Name of State"
                                                                                fieldLabel="State Name"
                                                                                name="./state"/>
                                                                            <path
                                                                                jcr:primaryType="nt:unstructured"
                                                                                sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
                                                                                fieldDescription="Select Path"
                                                                                fieldLabel="Path"
                                                                                name="./path"
                                                                                rootPath="/content"/>
                                                                        </items>
                                                                    </column>
                                                                </items>
                                                            </field>
                                                        </states>
                                                    </items>
                                                </column>
                                            </items>
                                        </field>
                                    </countries>
                                </items>
                            </column>
                        </items>
                    </fieldset>
                </items>
            </column>
        </items>
    </content>
</jcr:root>

5) Line 45 in above dialog xml marks this multifield as nested by setting a no value flag eaem-nested

6) Different layouts can be used to structure the multifield, here we use granite/ui/components/foundation/layouts/fixedcolumns layout

7) Create a clientlib (cq:ClientLibraryFolder) /apps/touch-ui-nested-multi-field-panel/clientlib with categories property as String with value cq.authoring.dialog and dependencies as String[] with value underscore

8) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/js.txt with

                nested-multifield.js

9) Create file (nt:file) /apps/touch-ui-nested-multi-field-panel/clientlib/nested-multifield.js with following code

(function () {
    var DATA_EAEM_NESTED = "data-eaem-nested";
    var CFFW = ".coral-Form-fieldwrapper";

    //reads multifield data from server, creates the nested composite multifields and fills them
    var addDataInFields = function () {
        $(document).on("dialog-ready", function() {
            var mName = $("[" + DATA_EAEM_NESTED + "]").data("name");

            if(!mName){
                return;
            }

            //strip ./
            mName = mName.substring(2);

            var $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']"),
                $form = $fieldSets.closest("form.foundation-form");

            var actionUrl = $form.attr("action") + ".json";

            var postProcess = function(data){
                if(!data || !data[mName]){
                    return;
                }

                var mValues = data[mName], $field, name;

                if(_.isString(mValues)){
                    mValues = [ JSON.parse(mValues) ];
                }

                _.each(mValues, function (record, i) {
                    if (!record) {
                        return;
                    }

                    if(_.isString(record)){
                        record = JSON.parse(record);
                    }

                    _.each(record, function(rValue, rKey){
                        $field = $($fieldSets[i]).find("[name='./" + rKey + "']");

                        if(_.isArray(rValue) && !_.isEmpty(rValue)){
                            fillNestedFields( $($fieldSets[i]).find("[data-init='multifield']"), rValue);
                        }else{
                            $field.val(rValue);
                        }
                    });
                });
            };

            //creates & fills the nested multifield with data
            var fillNestedFields = function($multifield, valueArr){
                _.each(valueArr, function(record, index){
                    $multifield.find(".js-coral-Multifield-add").click();

                    //a setTimeout may be needed
                    _.each(record, function(value, key){
                        var $field = $($multifield.find("[name='./" + key + "']")[index]);
                        $field.val(value);
                    })
                })
            };

            $.ajax(actionUrl).done(postProcess);
        });
    };

    var fillValue = function($field, record){
        var name = $field.attr("name");

        if (!name) {
            return;
        }

        //strip ./
        if (name.indexOf("./") == 0) {
            name = name.substring(2);
        }

        record[name] = $field.val();

        //remove the field, so that individual values are not POSTed
        $field.remove();
    };

    //for getting the nested multifield data as js objects
    var getRecordFromMultiField = function($multifield){
        var $fieldSets = $multifield.find("[class='coral-Form-fieldset']");

        var records = [], record, $fields, name;

        $fieldSets.each(function (i, fieldSet) {
            $fields = $(fieldSet).find("[name]");

            record = {};

            $fields.each(function (j, field) {
                fillValue($(field), record);
            });

            if(!$.isEmptyObject(record)){
                records.push(record)
            }
        });

        return records;
    };

    //collect data from widgets in multifield and POST them to CRX as JSON
    var collectDataFromFields = function(){
        $(document).on("click", ".cq-dialog-submit", function () {
            var $form = $(this).closest("form.foundation-form");

            var mName = $("[" + DATA_EAEM_NESTED + "]").data("name");
            var $fieldSets = $("[" + DATA_EAEM_NESTED + "][class='coral-Form-fieldset']");

            var record, $fields, $field, name, $nestedMultiField;

            $fieldSets.each(function (i, fieldSet) {
                $fields = $(fieldSet).children().children(CFFW);

                record = {};

                $fields.each(function (j, field) {
                    $field = $(field);

                    //may be a nested multifield
                    $nestedMultiField = $field.find("[data-init='multifield']");

                    if($nestedMultiField.length == 0){
                        fillValue($field.find("[name]"), record);
                    }else{
                        name = $nestedMultiField.find("[class='coral-Form-fieldset']").data("name");

                        if(!name){
                            return;
                        }

                        //strip ./
                        name = name.substring(2);

                        record[name] = getRecordFromMultiField($nestedMultiField);
                    }
                });

                if ($.isEmptyObject(record)) {
                    return;
                }

                //add the record JSON in a hidden field as string
                $('<input />').attr('type', 'hidden')
                    .attr('name', mName)
                    .attr('value', JSON.stringify(record))
                    .appendTo($form);
            });
        });
    };

    $(document).ready(function () {
        addDataInFields();
        collectDataFromFields();
    });

    //extend otb multifield for adjusting event propagation when there are nested multifields
    //for working around the nested multifield add and reorder
    CUI.Multifield = new Class({
        toString: "Multifield",
        extend: CUI.Multifield,

        construct: function (options) {
            this.script = this.$element.find(".js-coral-Multifield-input-template:last");
        },

        _addListeners: function () {
            this.superClass._addListeners.call(this);

            //otb coral event handler is added on selector .js-coral-Multifield-add
            //any nested multifield add click events are propagated to the parent multifield
            //to prevent adding a new composite field in both nested multifield and parent multifield
            //when user clicks on add of nested multifield, stop the event propagation to parent multifield
            this.$element.on("click", ".js-coral-Multifield-add", function (e) {
                e.stopPropagation();
            });

            this.$element.on("drop", function (e) {
                e.stopPropagation();
            });
        }
    });

    CUI.Widget.registry.register("multifield", CUI.Multifield);
})();

10) The function collectDataFromFields() registers a click listener on dialog submit, to collect the multifield form data, create a json to group it and add in a hidden field before data is POSTed to CRX. The function addDataInFields() reads json data added previously and fills the multifield & nested multifield fields, when dialog is opened. Extension uses simple jquery & underscore js calls to get and set data; based on the widget type more code may be necessary for supporting complex Granite UI widgets (granite/ui/components/foundation/form)

11) Create the component jsp /apps/touch-ui-nested-multi-field-panel/sample-nested-multi-field/sample-nested-multi-field.jsp for rendering data entered in dialog

<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
    <b>Countries and States</b>

<%
        try {
            Property property = null;

            if (currentNode.hasProperty("countries")) {
                property = currentNode.getProperty("countries");
            }

            if (property != null) {
                JSONObject country = null, state = null;
                Value[] values = null;

                if (property.isMultiple()) {
                    values = property.getValues();
                } else {
                    values = new Value[1];
                    values[0] = property.getValue();
                }

                for (Value val : values) {
                    country = new JSONObject(val.getString());
%>
                    Country : <b><%= country.get("country") %></b>
<%
                    if (country.has("states")) {
                        JSONArray states = (JSONArray) country.get("states");

                        if (states != null) {
                            for (int index = 0, length = states.length(); index < length; index++) {
                                state = (JSONObject) states.get(index);
%>
                                <div style="padding-left: 25px">
                                    <a href="<%= state.get("path") %>.html" target="_blank">
                                        <%= state.get("state") %> - <%= state.get("path") %>
                                    </a>
                                </div>
<%
                            }
                        }

                    }
                }
            } else {
%>
                Add values in dialog
<%
            }
        } catch (Exception e) {
            e.printStackTrace(new PrintWriter(out));
        }
%>
</div>