AEM 61 - This And That



AEM_61_RECOMPILE_JSPS

1)  In AEM <= 6.0 the compiled classes are placed in CRX /var/classes - http://localhost:4502/crx/de/index.jsp#/var/classes. To force recompile any jsp files, deleting the node in /var/classes helped; with AEM 61, the compiled class files are NOT placed in /var/classes any more, so to recompile jsps, use Felix Console - http://localhost:4502/system/console/slingjsp or to be 100% sure to the naked eye, follow these steps

             a. Stop bundle org.apache.sling.commons.fsclassloader
             b. Search with keyword classes in CQ install folder <author>\crx-quickstart\launchpad\felix
             c. Delete generated java/class files from <author>\crx-quickstart\launchpad\felix\<bundleXYZ>\data\classes
             d. Delete generated java files from /var/classes in CRX (sightly generated java files are placed here)
             e. Restart bundle org.apache.sling.commons.fsclassloader




AEM_61_OSGI_INSTALL_ACTION

2)  OSGI action when updating packages in AEM. Packages with SNAPSHOT in name have special meaning; SNAPSHOT packages are only for Dev environment. Thanks to Ian Boston for the tip

Current VersionNew VersionAction
1.0.221.0.23Install
1.0.221.0.23-SNAPSHOTInstall
1.0.231.0.23-SNAPSHOTIgnore
1.0.23-SNAPSHOT1.0.23-SNAPSHOTInstall
1.0.23-SNAPSHOT1.0.23-r231423423Install
1.0.23-r2314234231.0.23-r231423423Ignore
1.0.23-r2314234231.0.23-r231423424Install (last digit 4 > 3)
1.0.23-SNAPSHOT1.0.23-T1r231423423Install
1.0.23-T1r2314234231.0.23-T2r231423423Install (Patch revision number - T2r > T1r)




AEM_61_ADD_WORKFLOW_ADMINISTRATORS

3)  By default, only administrators can view all workflow instancesto add users/groups for administering workflows (view, terminate, monitor workflows created by other users), give necessary read/write permissions on /etc/workflow/instances and add user/group to Superuser of Adobe Granite Workflow Service - http://localhost:4502/system/console/configMgr/com.adobe.granite.workflow.core.WorkflowSessionFactory







4)  By default, when html tags like hyperlink from an external website are copied into Rich Text Editor (not source edit mode) all attributes are NOT copied (so you may only see href copied and not others ). To copy other attributes like say class, role etc. htmlPasteRules have to be defined in the edit plugin of RTE. For more options check the documentation



To copy css classes, make sure cssMode is defined






5)  To know the hotfixes and feature packs installed on AEM 

curl -u admin -s http://localhost:4502/crx/packmgr/list.jsp | jshon -e results -a -e downloadName -u | grep -E "hotfix|feature"

Sample output:
                     cq-6.1.0-apps-featurepack-1.0.10.zip
                     cq-6.1.0-apps-featurepack-2.0.4.zip
                     cq-6.1.0-featurepack-6563-1.0.0.zip
                     cq-6.1.0-hotfix-6445-1.0.zip
                     cq-6.1.0-hotfix-6449-1.2.zip
                     cq-6.1.0-hotfix-6500-1.0.zip
                     cq-6.1.0-hotfix-6500-1.5.zip
                     cq-6.1.0-hotfix-6570-1.2.zip
                     cq-6.1.0-hotfix-6680-1.0.zip
                     cq-6.1.0-hotfix-6680-1.2.zip
                     cq-6.1.0-hotfix-7285-1.0.zip
                     cq-6.1.0-hotfix-7432-1.0.2.zip
                     cq-6.1.0-hotfix-6446-1.0.zip





6)  In order to reduce time spent on offline compaction, make sure the customer has latest Oak version and also they use memory mapped file access option with compaction command.  Since memory mapped file access would use off-heap memory, it would benefit to have high memory on the server - thanks Can Altuner for the tip

java -Dtar.memoryMapped=true -Dupdate.limit=5000000 -Dcompaction-progress-log=1500000 
     -Dcompress-interval=10000000 -Doffline-compaction=true -Xmx8G -jar oak-run-1.2.7.jar 
     compact /crx-quickstart/repository/segmentstore





7)  To validate user's password during reset or while creation, use password validation action of Authorizable Action Provider - http://localhost:4502/system/console/configMgr/org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider

For detailed error description on Touch UI - check this post, Classic UI - check this post

Configure



TouchUI Error



ClassicUI Error








8)  When a 300 Multiple choices status code is returned by json renderer of GET servlet, try changing your application logic or increase the JSON Max Results of Apache Sling GET Servlet - http://localhost:4502/system/console/configMgr/org.apache.sling.servlets.get.DefaultGetServlet

Demo






9)  CRXDE Lite Tools -> Query provides interface for running search queries; if a query is running for long time (could be because of missing index) and consuming system resources, the following Traversing Cursor warnings are logged in error.log

29.02.2016 11:48:22.174 *WARN* [0:0:0:0:0:0:0:1 [1456768102069] GET /bin/querybuilder.json HTTP/1.1] org.apache.jackrabbit.oak.spi.query.Cursors$TraversingCursor Traversed 12000 nodes with filter Filter(query=select [jcr:path], [jcr:score], * from [nt:base] as a where [modelId] = '/etc/workflow/models/request_for_activation/jcr:content/model' and [data/payload/path] = '/content/dam/geometrixx/portraits/alison_parker.jpg' and isdescendantnode(a, '/etc/workflow/instances') order by [endTime] desc /* xpath: /jcr:root/etc/workflow/instances//*[@modelId = '/etc/workflow/models/request_for_activation/jcr:content/model' and data/payload/@path = '/content/dam/geometrixx/portraits/alison_parker.jpg'] order by @endTime descending */, path=/etc/workflow/instances//*, property=[modelId=[/etc/workflow/models/request_for_activation/jcr:content/model], data/payload/path=[/content/dam/geometrixx/portraits/alison_parker.jpg]]); consider creating an index or changing the query

To kill such queries without restarting CQ

       a. Access QueryEngineSettings MBean - http://localhost:4502/system/console/jmx/org.apache.jackrabbit.oak%3Aname%3Dsettings%2Ctype%3DQueryEngineSettings
       b. Set the values LimitInMemory and LimitReads to some lower number like 10000
       c. Queries that exceed one of the limits are cancelled with an UnsupportedOperationException



To automatically kill long running queries - set the startup system properties -Doak.queryLimitInMemory and -Doak.queryLimitReads to some lower number like 10000 (requires restart)

For more information check helpx.adobe.com and jackrabbit.apache.org




10) To workaround org.apache.sling.api.request.TooManyCallsException, increase the Number of Calls per request - sling.max.calls, setting of Apache Sling Main Servlet http://localhost:4502/system/console/configMgr/org.apache.sling.engine.impl.SlingMainServlet

`27.01.2016 15:29:50.021 *ERROR* [0:0:0:0:0:0:0:1 [1453930186556] GET /apps/geometrixx/components/testComponent/_cq_dialog.html/content/geometrixx/en/jcr:content/par/testComponent HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl service: Uncaught SlingException
org.apache.sling.api.request.TooManyCallsException: /libs/granite/ui/components/foundation/form/fieldset/fieldset.jsp
at org.apache.sling.engine.impl.request.RequestData.service(RequestData.java:517)
at org.apache.sling.engine.impl.filter.SlingComponentFilterChain.render(SlingComponentFilterChain.java:44)
at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:77)
at com.day.cq.personalization.impl.TargetComponentFilter.doFilter(TargetComponentFilter.java:96)
at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:68)
at com.day.cq.wcm.core.impl.WCMDebugFilter.doFilter(WCMDebugFilter.java:146)








11)  To find the jar file in cq install folder, containing a specific class - look for the bundle id folder


 

AEM 61 - TouchUI Date Picker Validator Comparing Two Date Fields in Dialog

Goal


Add a Validator on Granite (Coral) Date Picker in TouchUI Dialogs

Page properties dialog has two fields OnTime and OffTime for Activation / Deactivation; for demo purposes, validator here checks if date of OffTime is lesser than OnTime

Demo shows dialog of foundation page component modified to add the eaemCheckDateAfter config (/libs/foundation/components/page/cq:dialog/content/items/tabs/items/basic/items/column/items/onofftime/items/offdate). This is just for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered

Demo | Package Install


Configuration

Set the property eaemCheckDateAfter value to the name of dialog property to check against. Here its set to ./onTime



Inline Dialog - Error Shown when Off Time < On Time



Full Screen Dialog - Error Shown when Off Time < On Time



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-date-picker-validator

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-date-picker-validator/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

3) Create file ( type nt:file ) /apps/touchui-date-picker-validator/clientlib/js.txt, add the following

                         date-validator.js

4) Create file ( type nt:file ) /apps/touchui-date-picker-validator/clientlib/date-validator.js, add the following code

(function ($) {
    var EAEM_CHECK_DATE_AFTER = "eaemcheckdateafter",
        fieldErrorEl = $("<span class='coral-Form-fielderror coral-Icon coral-Icon--alert coral-Icon--sizeS' " +
                            "data-init='quicktip' data-quicktip-type='error' />");

    $.validator.register({
        selector: "input",
        validate: validate, //if validate() returns a non empty value, show() is called
        show: show,
        clear: clear
    });

    function validate($el){
        //if not date widget or widget value is empty, return
        if(!$el.parent().hasClass("coral-DatePicker") || _.isEmpty($el.val())){
            return;
        }

        var $datePicker = $el.parent(),
            $form = $datePicker.closest("form"),
            checkDateAfter = $datePicker.data(EAEM_CHECK_DATE_AFTER);

        if(_.isEmpty(checkDateAfter)){
            return;
        }

        var $toCompareField = $form.find("[name='" + checkDateAfter + "']");

        if(_.isEmpty($toCompareField) || _.isEmpty($toCompareField.val())){
            return;
        }

        var toCompareMillis = new Date($toCompareField.val()).getTime(),
            compareWithMillis = new Date($el.val()).getTime(),
            text = $toCompareField.closest(".coral-Form-fieldwrapper").find(".coral-Form-fieldlabel").html();

        return ( compareWithMillis < toCompareMillis) ? "Should not be less than '" + text + "'" : null;
    }

    function show($el, message){
        if(!$el.parent().hasClass("coral-DatePicker")){
            return;
        }

        var $datePicker = $el.parent();

        this.clear($el);

        var arrow = $datePicker.closest("form").hasClass("coral-Form--vertical") ? "right" : "top";

        $el.attr("aria-invalid", "true").toggleClass("is-invalid", true);

        fieldErrorEl.clone()
            .attr("data-quicktip-arrow", arrow)
            .attr("data-quicktip-content", message)
            .insertAfter($datePicker);
    }

    function clear($el){
        if(!$el.parent().hasClass("coral-DatePicker")){
            return;
        }

        var $datePicker = $el.parent();

        $el.removeAttr("aria-invalid").removeClass("is-invalid");

        $datePicker.nextAll(".coral-Form-fielderror").tooltip("hide").remove();
    }
}(jQuery));

AEM 61 - TouchUI Slide Show Component with Image Multifield

Goal


Create a TouchUI slideshow component rendered using Sightly, images added using Image Multifield

This post focuses on creating a slideshow using sightly templating language with the images added in multifield. For more information on image multifield widget check this post

For ClassicUI slide show component check this post

To learn more about sightly controls check adobe documentation

Hotfix 6670 must be installed for this widget extension to work

Demo | Package Install

For 61 SP1 (no hotfixes necessary) - Package Install







Solultion


1) Assuming user added the images for slideshow component using image multifield widget; the following structure gets created in CRX



2) To render images using html, create the component /apps/touchui-sightly-image-multifield/sample-image-multifield with following structure



3)  The file /apps/touchui-sightly-image-multifield/sample-image-multifield/sample-image-multifield.html uses sightly to create html. Add the following code to prepare html for slideshow..

<div    data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}"
        style="display: block; padding: 10px">

    <sly data-sly-call="${clientLib.all @ categories='eaem.slideshow'}" />

    <b>Image Multi Field Sample</b>

    <div data-sly-use.images="imgHelper.js" data-sly-unwrap>
        <div data-sly-test="${images.gallery}" style="margin-bottom: 15px">
            Gallery - ${images.gallery}
        </div>

        <div data-sly-test="${!images.artists && wcmmode.edit}">
            Add images using component dialog
        </div>

        <div data-sly-test="${images.artists}" class="eaem-slideshow">
            <div data-sly-list.artist="${images.artists}">
                <div class="show-pic ${artistList.first ? 'active' : ''}">
                    <img src="${artist.painting}" height="530px" width="700px"/>
                    <div class="overlayText">
                        <span class="overlayTitle">${artist.name}</span>
                        <div class="overlayDesc">${artist.desc}</div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

4) #4 adds the clientlib eaem.slideshow with necessary css and js logic (available in package install) to run a simple slideshow with images. #8 initializes helper object (defined in java script) and exposes it through the variable images using sightly use-api

5) Create file /apps/touchui-sightly-image-multifield/sample-image-multifield/imgHelper.js with necessary logic to read the CRX node structure and create a js object for sightly script added in step3

"use strict";

use( ["/libs/wcm/foundation/components/utils/ResourceUtils.js" ], function(ResourceUtils){
    var images = {}, properties = granite.resource.properties,
        artistsPath = granite.resource.path + "/artists", counter = 1, artist;

    images.gallery = properties["gallery"];
    images.artists = undefined;

    function recursiveImageRead(path){
        ResourceUtils.getResource(path)
                        .then(addImage);
    }

    function addImage(artistRes){
        if(!images.artists){
            images.artists = [];
        }

        properties = artistRes.properties;

        artist = {
            painting: properties["paintingRef"],
            desc: properties["desc"],
            name: properties["artist"]
        };

        images.artists.push(artist);

        recursiveImageRead(artistsPath + "/" + counter++);
    }

    recursiveImageRead(artistsPath + "/" + counter++);

    return images;
} );


AEM 61 - TouchUI Assets Console show node name instead of dc:title or jcr:title

Goal


In all 3 views (card, list, column) of Touch UI Assets Console, show the node name (filename) and NOT dc:title for assets or jcr:title for folders

For AEM 62 check this post

Demo | Package Install


Product



Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-assets-show-filenames

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-assets-show-filenames/clientlib, set a property categories of String type to granite.ui.foundation.admin and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touchui-assets-show-filenames/clientlib/js.txt, add the following

                         show-file-name.js

4) Create file ( type nt:file ) /apps/touchui-assets-show-filenames/clientlib/show-file-name.js, add the following code

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

    var DAM_ADMIN_CHILD_PAGES = "cq-damadmin-admin-childpages",
        LOAD_EVENT = "coral-columnview-load",
        FOUNDATION_LAYOUT_CARD = ".foundation-layout-card",
        NS_COLUMNS = ".foundation-layout-columns";

    $document.on("foundation-mode-change", function(e, mode, group){
        //not assets console, return
        if(group != DAM_ADMIN_CHILD_PAGES){
            return;
        }

        showFileName();

        var $collection = $(".foundation-collection[data-foundation-mode-group=" + group + "]");

        //for column view
        $collection.on(LOAD_EVENT, function(){
            setTimeout( showFileName ,200);
        });

        //for column view select
        $collection.on("coral-columnview-item-select" + NS_COLUMNS, showFileName);

        if (!$collection.is(FOUNDATION_LAYOUT_CARD)) {
            return;
        }

        var $scrollContainer = $collection.parent();

        //for card view scroll
        $scrollContainer.on("scroll" + FOUNDATION_LAYOUT_CARD, _.throttle(function(){
            var paging = $collection.data("foundation-layout-card.internal.paging");

            if(!paging.isLoading){
                return;
            }

            var INTERVAL = setInterval(function(){
                if(paging.isLoading){
                    return;
                }

                clearInterval(INTERVAL);

                showFileName();
            }, 250);
        }, 100));
    });

    function showFileName(){
        var $articles = $("article"), $article, name;

        $articles.each(function(index, article){
            $article = $(article);

            name = getStringAfterLastSlash($article.data("path"));

            $article.find("h4").html(name);
            $article.find(".coral-ColumnView-label").html(name);
        });
    }

    function getStringAfterLastSlash(str){
        if(!str || (str.indexOf("/") == -1)){
            return "";
        }

        return str.substr(str.lastIndexOf("/") + 1);
    }
})(jQuery, jQuery(document));

AEM 61 - TouchUI Rich Text Editor Remove Unused Plugins

Goal


A sample extension for removing unused plugins in RTE (Rich Text Editor) of Touch UI. With the extension Inline Editor is stripped to have buttons for just full screen, close & save; Fullscreen Editor with only switch to inline...

Demo | Package Install


Inline Editor



Fullscreen Editor



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touchui-hide-default-rte-plugins

2) Create node /apps/touchui-hide-default-rte-plugins/clientlib of type cq:ClientLibraryFolder and add a String property categories with value rte.coralui2

3) Create file (nt:file) /apps/touchui-hide-default-rte-plugins/clientlib/js.txt and add

                       remove-plugins.js

4) Create file (nt:file) /apps/touchui-hide-default-rte-plugins/clientlib/remove-plugins.js and add the following code.

(function(){
    var INLINE_TOOLBAR = [ "fullscreen#start", "control#close", "control#save"],
        FULLSCREEN_TOOLBAR = [ "fullscreen#finish"];

    var EAMCuiToolbarBuilder = new Class({
        toString: "EAEMCuiToolbarBuilder",

        extend: CUI.rte.ui.cui.CuiToolbarBuilder,

        _getUISettings: function(options) {
            var uiSettings = this.superClass._getUISettings(options);

            //inline toolbar - "#format", "#justify", "#lists", "links#modifylink",
            // "links#unlink", "fullscreen#start", "control#close", "control#save"
            uiSettings["inline"]["toolbar"] = INLINE_TOOLBAR.slice(0);

            //fullscreen toolbar - "format#bold", "format#italic", "format#underline",
            // "fullscreen#finish"....
            uiSettings["fullscreen"]["toolbar"] = FULLSCREEN_TOOLBAR.slice(0);

            return uiSettings;
        }
    });

    var EAEMToolkitImpl = new Class({
        toString: "EAEMToolkitImpl",

        extend: CUI.rte.ui.cui.ToolkitImpl,

        createToolbarBuilder: function() {
            return new EAMCuiToolbarBuilder();
        }
    });

    CUI.rte.ui.ToolkitRegistry.register("cui", EAEMToolkitImpl);
}());




AEM 61 - Classic UI Disable Drag and Drop from Desktop to DAM Admin Console

Goal


Disable upload of assets using Drag and Drop from Desktop to DAM Admin Console - http://localhost:4502/damadmin

For TouchUI check this post

Demo | Package Install




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classicui-assets-disable-desktop-drop

2) Create node /apps/classicui-assets-disable-desktop-drop/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/classicui-assets-disable-desktop-drop/clientlib/js.txt and add

                       disable-drop.js

4) Create file (nt:file) /apps/classicui-assets-disable-desktop-drop/clientlib/disable-drop.js and add the following code.

(function () {
    if (window.location.pathname !== "/damadmin") {
        return;
    }

    //id set in /libs/wcm/core/content/damadmin
    var DAM_ADMIN_ID = "cq-damadmin";

    function handleDrops() {
        var damAdmin = CQ.Ext.getCmp(DAM_ADMIN_ID);

        damAdmin.html5UploadFiles = function (files) {
            CQ.Ext.Msg.alert("Drop Error", "Drop from desktop disabled");
        }
    }

    var INTERVAL = setInterval(function () {
        var grid = CQ.Ext.getCmp(DAM_ADMIN_ID + "-grid");

        if (!grid) {
            return;
        }

        clearInterval(INTERVAL);

        try {
            handleDrops();
        } catch (err) {
            console.log("error executing drag drop disable extension");
        }
    }, 250);
})();

AEM 61 - TouchUI Disable Drag and Drop from Desktop to Assets Console

Goal


Disable upload of assets using Drag and Drop from Desktop to Assets Console - http://localhost:4502/assets.html/content/dam

For ClassicUI check this post

Demo | Package Install



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touchui-assets-disable-desktop-drop

2) Create node /apps/touchui-assets-disable-desktop-drop/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.gui.damadmin.admin

3) Create file (nt:file) /apps/touchui-assets-disable-desktop-drop/clientlib/js.txt and add

                       disable-drop.js

4) Create file (nt:file) /apps/touchui-assets-disable-desktop-drop/clientlib/disable-drop.js and add the following code.

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

    $document.on("foundation-contentloaded", function () {
        $document.off("dropzonedragover", "span.coral-FileUpload");
        $document.on("dropzonedragover", "span.coral-FileUpload", showDisabledMessage);

        //triggered by coral when dropped files are added in queue and ready to process
        $document.off("filelistprocessed", "span.coral-FileUpload")
                        .on("filelistprocessed", "span.coral-FileUpload", clearAndRefresh);
    });

    function clearAndRefresh(event){
        var fileUpload = event.fileUpload;

        if(fileUpload && (fileUpload.isDragOver === true)){
            fileUpload.uploadQueue.splice(0, fileUpload.uploadQueue.length);
            $(".foundation-content").adaptTo("foundation-content").refresh();
        }
    }

    function showDisabledMessage() {
        var message = $('<div class=\"drag-drop-message\"><h1>'
                            + '<span>{</span>Drop from desktop disabled<span>}</span>' +
                        '</h1></div>');
        $('.foundation-collection-container').overlayMask('show', message);
    }
})(jQuery, $(document));


AEM 61 - Touch UI Rich Text Editor Remove Bold and Add Strong Plugin

Goal


Remove the Bold plugin in TouchUI Rich Text Editor and add Strong plugin for semantic markup

AEM RTE Bold Plugin wraps text with "b" tag by default, to make the plugin wrap text with strong tag, add nt:unstructured nodes htmlRules/docType/typeConfig with property useSemanticMarkup=true; if you are happy with it, avoid this extension

Demo shows dialog of foundation text component modified to add the plugin config (/libs/foundation/components/text/dialog/items/tab1/items/text/rtePlugins). This is just for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered...

Demo (download for better view) | Package Install


Plugin Configuration



Semantic Markup Configuration



Inline Editor

Full Screen Editor




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touchui-rte-strong-plugin

2) Create node /apps/touchui-rte-strong-plugin/clientlib of type cq:ClientLibraryFolder and add a String property categories with value rte.coralui2

3) Create file (nt:file) /apps/touchui-rte-strong-plugin/clientlib/js.txt and add

                       strong.js

4) Create file (nt:file) /apps/touchui-rte-strong-plugin/clientlib/strong.js and add the following code.

(function($, CUI){
    var ExperienceAEM = {
        GROUP: "experience-aem",
        STRONG_FEATURE: "strong",
        BOLD: "format#bold"
    };

    function getStrongFeature() {
        return ExperienceAEM.GROUP + "#" + ExperienceAEM.STRONG_FEATURE;
    }
 
    //extend toolbar builder to register strong styles
    ExperienceAEM.CuiToolbarBuilder = new Class({
        toString: "EAEMCuiToolbarBuilder",
 
        extend: CUI.rte.ui.cui.CuiToolbarBuilder,
 
        //add strong icon to the existing set
        _getUISettings: function(options) {
            var uiSettings = this.superClass._getUISettings(options),
                strongFeature = getStrongFeature();

            //inline toolbar
            var items = uiSettings["inline"]["popovers"]["format"].items;

            //insert strong feature
            if(items.indexOf(strongFeature) == -1){
                items.push(strongFeature);
            }

            //remove bold feature
            if(items.indexOf(ExperienceAEM.BOLD) >= 0){
                items = items.splice(items.indexOf(ExperienceAEM.BOLD), 1);
            }

            //fullscreen toolbar
            items = uiSettings["fullscreen"]["toolbar"];

            //insert strong feature
            if(items.indexOf(strongFeature) == -1){
                items.splice(3, 0, strongFeature);
            }

            //remove bold feature
            if(items.indexOf(ExperienceAEM.BOLD) >= 0){
                items = items.splice(items.indexOf(ExperienceAEM.BOLD), 1);
            }

            if(!this._getClassesForCommand(strongFeature)){
                this.registerAdditionalClasses(strongFeature, "coral-Icon coral-Icon--textStyle");
            }

            return uiSettings;
        }
    });
 
    ExperienceAEM.ToolkitImpl = new Class({
        toString: "EAEMToolkitImpl",
 
        extend: CUI.rte.ui.cui.ToolkitImpl,
 
        createToolbarBuilder: function() {
            return new ExperienceAEM.CuiToolbarBuilder();
        }
    });
 
    CUI.rte.ui.ToolkitRegistry.register("cui", ExperienceAEM.ToolkitImpl);
 
    ExperienceAEM.TouchUIStrongPlugin = new Class({
        toString: "TouchUIStrongPlugin",
 
        extend: CUI.rte.plugins.Plugin,
 
        pickerUI: null,
 
        getFeatures: function() {
            return [ ExperienceAEM.STRONG_FEATURE ];
        },
 
        initializeUI: function(tbGenerator) {
            var plg = CUI.rte.plugins;
 
            if (this.isFeatureEnabled(ExperienceAEM.STRONG_FEATURE)) {
                this.pickerUI = tbGenerator.createElement(ExperienceAEM.STRONG_FEATURE, this, true, "Wrap Strong");
                tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT, this.pickerUI, 120);
            }
        },
 
        execute: function(id) {
            this.editorKernel.relayCmd(id);
        },
 
        //to mark the strong icon selected/deselected
        updateState: function(selDef) {
            var hasUC = this.editorKernel.queryState(ExperienceAEM.STRONG_FEATURE, selDef);
 
            if (this.pickerUI != null) {
                this.pickerUI.setSelected(hasUC);
            }
        },
 
        notifyPluginConfig: function(pluginConfig) {
            pluginConfig = pluginConfig || { };
 
            var defaults = {
                "tooltips": {
                    "strong": {
                        "title": "Wrap Strong",
                        "text": "Wrap Strong"
                    }
                }
            };
 
            CUI.rte.Utils.applyDefaults(pluginConfig, defaults);
 
            this.config = pluginConfig;
        }
    });
 
    CUI.rte.plugins.PluginRegistry.register(ExperienceAEM.GROUP, ExperienceAEM.TouchUIStrongPlugin);
 
    ExperienceAEM.StrongCmd = new Class({
        toString: "StrongCmd",
 
        extend: CUI.rte.commands.Command,
 
        isCommand: function(cmdStr) {
            return (cmdStr.toLowerCase() == ExperienceAEM.STRONG_FEATURE);
        },
 
        getProcessingOptions: function() {
            var cmd = CUI.rte.commands.Command;
            return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
        },
 
        _getTagObject: function() {
            return {
                "tag": "strong"
            };
        },
 
        execute: function(execDef) {
            var selection = execDef.selection;
 
            if (!selection) {
                return;
            }
 
            var nodeList = execDef.nodeList;
 
            if (!nodeList) {
                return;
            }
 
            var common = CUI.rte.Common;
            var context = execDef.editContext;
 
            var tagObj = this._getTagObject();
 
            var tags = common.getTagInPath(context, selection.startNode, tagObj.tag, tagObj.attributes);
 
            if (tags == null) {
                nodeList.surround(execDef.editContext, tagObj.tag, tagObj.attributes);
            } else {
                nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, tagObj.attributes, true);
            }
        },
 
        queryState: function(selectionDef) {
            var common = CUI.rte.Common;
            var context = selectionDef.editContext;
 
            var selection = selectionDef.selection;
            var tagObj = this._getTagObject();
 
            return (common.getTagInPath(context, selection.startNode, tagObj.tag, tagObj.attributes) != null);
        }
    });
 
    CUI.rte.commands.CommandRegistry.register(ExperienceAEM.STRONG_FEATURE, ExperienceAEM.StrongCmd);
})($, window.CUI);

AEM 61 - Setup Groovy Scripts Debugging in IDEA for Felix Script Console

Goal


This post is on creating Data Sanity Check Scripts for a project's Runbook. There might be cases when you have to check the validity of CRX node structures (created manually or using CURL) before running pretty intensive logic on the node structures. Running such test scripts is to ensure any avoidable mistakes are found in advance, most of the times a simple query is good enough for finding out if the structure was created as expected; for example, to check if the nodes have property eaem:imageURL, the following sql2 query should suffice

SELECT * FROM [sling:OrderedFolder] WHERE ISDESCENDANTNODE('/content/dam/styles') AND [eaem:imageURL] IS NULL

For complex checks, you may have to provide a script to the customer (or run it yourself). One such check is, find if the nested node structure has duplicate nodes; the example discussed in next steps has style number node 1272336 mistakenly added in two places /content/dam/styles/accessories/1272336, /content/dam/styles/bottoms/1272336; These invalid node entries cannot be found using JCR queries (if you can write one, please leave a comment...)

There are many ways to create such a thing, but come with arguable disadvantages...

1) Http Servlet or JSP - Simple, but cannot be part of production packages; if code should be enhanced or bugs fixed, the customer may not be good with adding such packages on production systems, adhoc

2) DavEx Client or RMI Client - Easy to code and very useful for developers, but not all customers may have java installed locally to run it and also watch out for jdk version, linkage errors; gets real messy sometimes

There should be a simple way to create/run random scripts than can do sanity checks, handed over to the customer via email (yes!!) without the code being part of release cycles. Two ways of doing it, that i know of...

1) ACS AEM Tools Fiddle - Extremely useful on Dev, QA & Staging environments; but the Tools Team does not recommend installing it on production systems

2) Groovy Script Console - Lesser of two evils, a relatively secure and easy way to run random scripts; as the console is available to OSGI admins only, chances of doing something nasty corrupting the whole production system are minimal

This post is on adding Groovy Felix Script Console to AEM and creating/debugging the script in IDE Intellij IDEA to Find Duplicate Node Names in CRX

Thank you Chetan Mehrotra for the Groovy bundles and valuable tips

Demo | Groovy Script Console Package Install

Solution


1) Login to CRXDE Lite, install the Groovy Script Console Package or get the bundles here; Bundle for Script Console and Groovy Scripting Support bundle get installed



2) The console should now be accessible at http://localhost:4502/system/console/sc




3) Download the Groovy SDK installer or binary package and install it onto local file system eg. on windows - C:\dev\code\install\groovy-2.4.5

4) Create a plain Java module test-groovy in IntelliJ IDEA IDE and set the Dependencies to Groovy sdk, DavEx jars




5) Create file find-duplicates.groovy in module test-groovy and add the following code

import javax.jcr.*
import org.apache.jackrabbit.commons.JcrUtils

import javax.jcr.query.QueryResult
import javax.jcr.query.Row

import org.apache.sling.jcr.api.SlingRepository

Session session
def styles = [:] as HashMap

try {
    def queryStr = 'SELECT * FROM [sling:OrderedFolder] WHERE ISDESCENDANTNODE("/content/dam/styles")'

    //session = getSlingRepoSession()
    session = getDavExRepoSession()

    def qm = session.workspace.queryManager
    def query = qm.createQuery(queryStr,'JCR-SQL2')

    QueryResult result = query.execute()

    result.rows.each{
        Row r ->

        if (styles.containsKey(r.node.name)){
            println "Duplicate style number $r.node.name, path $r.node.path; Style Exists " + styles.get(r.node.name)
        }else{
            styles.put(r.node.name, r.node.path)
        }
    }
}catch(e){
    println e
}finally{
    session?.logout()
}

def getSlingRepoSession(){
    SlingRepository repo = osgi.getService(SlingRepository.class)
    return repo.loginAdministrative(null)
}

def getDavExRepoSession(){
    def REPO = "http://localhost:4502/crx/server"
    Repository repo = JcrUtils.getRepository(REPO)
    repo.login(new SimpleCredentials("admin", "admin".toCharArray()), "crx.default")
}

6) In the above script there are two ways of getting a JCR session; As shown in demo, uncomment #16, session = getDavExRepoSession() when debugging the script from IDE; Script executed in Felix Script Console (added in Project's Runbook) will have #15, session = getSlingRepoSession() uncommented



7) The script when executed in Felix Script Console for Groovy outputs the duplicate node names (styles) so that customer can correct node structures before initiating some process intensive execution...





AEM 61 - Sample Granite Widget In Touch UI extending Multifield to Limit Items

Goal


Create Touch UI Limit Multifield widget extending Granite Multifield widget, for limiting the number of items added in a multifield

Unlike Classic UI xtypes, widgets in Granite framework work with server-side rendering principles (html prepared on the server and executed on client). This sample Touch UI Granite widget is analogous to Classic UI xtype with following inheritance

/apps/touchui-limit-multifield ->  /libs/granite/ui/components/foundation/form/multifield -> /libs/granite/ui/components/foundation/form/field

Dialog of foundation page component (/libs/foundation/components/page/cq:dialog/content/items/tabs/items/basic/items/column/items/vanityurl/items/vanitypath) modified for demonstration only (on Geometrixx pages), ideally the foundation components should never be altered...

For Classic UI check this post

Demo | Package Install


Configure fieldLimit, set sling:resourceType to /apps/touchui-limit-multifield




Page Properties Dialog Validation Error


Page Properties Dialog Full Screen Validation Error


Solution


1) Login to CRXDE Lite, create nt:folder /apps/touchui-limit-multifield

2) Create nt:file /apps/touchui-limit-multifield/touchui-limit-multifield.jsp, add the following code. JS can be separated out into a clientlib

<%@ page import="com.adobe.granite.ui.components.Config" %>

<%@include file="/libs/granite/ui/global.jsp" %>

<%--include ootb multifield--%>
<sling:include resourceType="/libs/granite/ui/components/foundation/form/multifield"/>

<%
    Config mCfg = cmp.getConfig();

    Resource mField = mCfg.getChild("field");

    ValueMap mVM = mField.getParent().adaptTo(ValueMap.class);

    String mFieldLimit = mVM.get("fieldLimit", "");

    if(mFieldLimit.equals("")){
        return;
    }

    String countValidatorId = "multifield-validator-" + System.currentTimeMillis(); //or some random number
%>

<%--
coral validation framework ignores hidden and contenteditable fields, so add an invisible text field
the text field is just for registering a validator
--%>
<input type=text style='display:none' id="<%=countValidatorId%>"/>

<script>
    (function($){
        var fieldErrorEl = $("<span class='coral-Form-fielderror coral-Icon coral-Icon--alert coral-Icon--sizeS' " +
                                "data-init='quicktip' data-quicktip-type='error' />");

        var $countValidatorField = $("#<%=countValidatorId%>"),
            $multifield = $countValidatorField.prev().find(".coral-Multifield"),
            fieldLimit = parseInt($multifield.data("fieldlimit")),
            count = $multifield.find(".coral-Multifield-input").length;

        //add validator on the multifield
        $.validator.register({
            selector: $countValidatorField,
            validate: validate,
            show: show,
            clear: clear
        });

        $multifield.on("click", ".js-coral-Multifield-add", function (e) {
            ++count;
            adjustMultifieldUI();
        });

        $multifield.on("click", ".js-coral-Multifield-remove", function (e) {
            --count;
            adjustMultifieldUI();
        });

        function adjustMultifieldUI(){
            $countValidatorField.checkValidity();
            $countValidatorField.updateErrorUI();

            /*var $addButton = $multifield.find(".js-coral-Multifield-add");

            if(count >= fieldLimit){
                $addButton.attr('disabled','disabled');
            }else{
                $addButton.removeAttr('disabled');
            }*/
        }

        function validate(){
            if(count <= fieldLimit){
                return null;
            }

            return "Limit set to " + fieldLimit;
        }

        function show($el, message){
            this.clear($countValidatorField);

            var arrow = $multifield.closest("form").hasClass("coral-Form--vertical") ? "right" : "top";

            fieldErrorEl.clone()
                    .attr("data-quicktip-arrow", arrow)
                    .attr("data-quicktip-content", message)
                    .insertAfter($multifield);
        }

        function clear(){
            $multifield.nextAll(".coral-Form-fielderror").tooltip("hide").remove();
        }
    })(jQuery);
</script>

3) #6 for extending ootb mulitifield /libs/granite/ui/components/foundation/form/multifield using sling:include

4) #41 for registering a validator on the multifield, #48, #53 to trigger validator when user adds/removes items

5) To use the Limit Multifield in a TouchUI Dialog, set the following properties on node

                   sling:resourceType         /apps/touchui-limit-multifield
                   fieldLimit                 Integer eg. 3