AEM CQ 56 - Programmatically open a component dialog

Goal


Open a component dialog programmatically. To open a component dialog you generally do a double click, here a custom button click also opens the same dialog. Check the demo

Solution


1) In your jsp or where ever you want to open component dialog add the below simple js function. Here i added it in a text component jsp, and the button opens it's component dialog

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

<cq:text property="text"/>

<input type=button onClick="openComponentPropertiesDialog('<%= currentNode.getPath() %>')" value="Open Component Dialog"/>

<script>
    function openComponentPropertiesDialog(path){
        var editRollOver = CQ.utils.WCM.getEditables()[path];
        CQ.wcm.EditBase.showDialog(editRollOver, CQ.wcm.EditBase.EDIT);
    }
</script>





AEM CQ 56 - Dynamic chain select widget with combo boxes

Goal


Create a widget to chain combo boxes and populate combo box data dynamically. Here the lower level combo boxes data change based on values selected in the higher levels. Source code, Package Install and Demo video are available for download. Please leave a comment if you find bugs




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder (nt:folder) /apps/chainselect

2) Create folder (nt:folder) /apps/chainselect/install and deploy servlet GetDropDownData as OSGI component to return the data for combo boxes

package apps.mysample.chainselect;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@SlingServlet(
        paths="/bin/mycomponents/chainselect/dropdowndata",
        methods = "GET",
        metatype = true,
        label = "Dropdown Data Servlet"
)
public class GetDropDownData extends SlingAllMethodsServlet {
    private static final Logger LOG = LoggerFactory.getLogger(GetDropDownData.class);

    private static Map<String, String> LEVEL_1 = new HashMap<String, String>();
    private static Map<String, Map<String, String>> LEVEL_2 = new HashMap<String, Map<String, String>>();
    private static Map<String, Map<String, String>> LEVEL_3 = new HashMap<String, Map<String, String>>();

    static{
        fillStaticData();
    }

    private static void fillStaticData(){
        LEVEL_1.put("ENTERTAINMENT", "Entertainment");
        LEVEL_1.put("HEALTH", "Health");
        LEVEL_1.put("PARTY", "Party");

        Map<String, String> map = new LinkedHashMap<String, String>();

        map.put("MOVIES", "Movies");
        map.put("CELEB_NEWS", "Celebrity News");
        map.put("TV", "TV");
        map.put("MUSIC", "Music");
        map.put("STYLE", "Style");

        LEVEL_2.put("ENTERTAINMENT", map);

        map = new LinkedHashMap<String, String>();

        map.put("MENS_HEALTH", "Men's Health");
        map.put("WOMENS_HEALTH", "Women's Health");
        map.put("CHILD_HEALTH", "Children's Health");
        map.put("ALT_MEDICINE", "Alternative Medicine");

        LEVEL_2.put("HEALTH", map);

        map = new LinkedHashMap<String, String>();

        map.put("HOLLYWOOD", "Hollywood");
        map.put("BOLLYWOOD", "Bollywood");

        LEVEL_3.put("MOVIES", map);

        map = new LinkedHashMap<String, String>();

        map.put("MJ", "Michael Jackson");
        map.put("RAHMAN", "A R Rahman");

        LEVEL_3.put("MUSIC", map);
    }

    private void ouputInitData(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
                                throws ServletException{
        Integer level = NumberUtils.createInteger(request.getParameter("level"));
        String keyword = request.getParameter("keyword");

        if(level == null){
            level = 1;
        }

        try{
            JSONWriter jw = new JSONWriter(response.getWriter());
            Field field = null; Class clazz = this.getClass();
            Map<String, String> map = null;

            jw.object();

            do{
                try{
                    field = clazz.getDeclaredField("LEVEL_" + level);
                }catch (NoSuchFieldException nfe){
                    break;
                }

                if(level == 1){
                    map = (Map<String, String>)field.get(null);
                }else{
                    if(StringUtils.isEmpty(keyword)){
                        keyword = ((Map<String,Map<String, String>>)field.get(null)).keySet().iterator().next();
                    }

                    map = ((Map<String,Map<String, String>>)field.get(null)).get(keyword);
                }

                if(map == null){
                    break;
                }

                keyword = null;

                jw.key(level.toString()).array();

                for(Map.Entry<String, String> entry : map.entrySet()){
                    jw.array();
                    jw.value(entry.getKey()).value(entry.getValue());
                    jw.endArray();

                    if(StringUtils.isEmpty(keyword)){
                        keyword = entry.getKey();
                    }
                }

                jw.endArray();
                level++;
            }while(true);

            jw.endObject();
        }catch(Exception e){
            LOG.error("Error getting dropdown data",e);
            throw new ServletException(e);
        }
    }

    private void ouputSavedText(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
            throws ServletException{
        try {
            String[] lStrs = request.getParameter("levels").split(",");
            String[] keywords = request.getParameter("keywords").split(",");
            JSONWriter jw = new JSONWriter(response.getWriter());

            Field field = null; Class clazz = this.getClass();
            Map<String, String> map = null; Integer level = null;

            jw.object();

            for(int i = 0; i < lStrs.length; i++){
                level = NumberUtils.createInteger(lStrs[i]);

                try{
                    field = clazz.getDeclaredField("LEVEL_" + level);
                }catch (NoSuchFieldException nfe){
                    continue;
                }

                if(level == 1){
                    map = (Map<String, String>)field.get(null);
                }else{
                    map = ((Map<String,Map<String, String>>)field.get(null)).get(keywords[i - 1]);
                }

                if(map == null){
                    continue;
                }

                jw.key(level.toString()).array();

                for(Map.Entry<String, String> entry : map.entrySet()){
                    jw.array();
                    jw.value(entry.getKey()).value(entry.getValue());
                    jw.endArray();
                }

                jw.endArray();
            }

            jw.endObject();
        } catch (Exception e) {
            LOG.error("Error getting dropdown data", e);
            throw new ServletException(e);
        }
    }

        @Override
    protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        String lStr = request.getParameter("levels");

        if(StringUtils.isNotEmpty(lStr)){
            ouputSavedText(request, response);
        }else{
            ouputInitData(request, response);
        }
    }
}

3) In the above servlet each level (LEVEL_1, LEVEL_2 etc) data structure has necessary data for combo box at that level. So when n-level combo boxes are created, necessary n-level data structures should also exist for serving them. A single ajax call returns the data for all combo boxes

4) Create clientlib (type cq:ClientLibraryFolder) /apps/chainselect/clientlib and set property categories with value cq.widgets

5) Create file (nt:file) /apps/chainselect/clientlib/js.txt, add

                     chainselect.js

6) Create file (nt:file) /apps/chainselect/clientlib/chainselect.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.ChainSelect = CQ.Ext.extend(CQ.Ext.Panel, {
    panelValue: '',

    constructor: function(config){
        config = config || {};

        if(!config.levels){
            config.levels = "1";
        }

        config.levels = parseInt(config.levels, 10);

        ExperienceAEM.ChainSelect.superclass.constructor.call(this, config);
    },

    getValue: function () {
        var pData = {};

        this.items.each(function(i){
            if(!i.level || i.xtype !== "combo" || i.disabled){
                return;
            }

            pData[i.level] = i.getValue();
        });

        return $.isEmptyObject(pData) ? "" : JSON.stringify(pData);
    },

    setValue: function (value) {
        var pData = JSON.parse(value);
        var levels = "", keywords = "", x = "", combo;

        for(x in pData){
            if(pData.hasOwnProperty(x)){
                levels = levels + x + ",";
                keywords = keywords + pData[x] + ",";
            }
        }

        levels = levels.substring(0, levels.lastIndexOf(","));
        keywords = keywords.substring(0, keywords.lastIndexOf(","));

        var lData = this.getDropDownData({ levels : levels, keywords: keywords });

        for(x in lData){
            if(lData.hasOwnProperty(x)){
                combo = this.findBy(function(comp){
                    return comp["level"] == x;
                }, this);

                combo[0].store.loadData(lData[x], false);
            }
        }

        this.items.each(function(i){
            if(!i.level || i.xtype !== "combo"){
                return;
            }

            if(pData[i.level]){
                i.setValue(pData[i.level]);
            }else{
                i.setDisabled(true);
            }
        });
    },

    validate: function(){
        return true;
    },

    getName: function(){
        return this.name;
    },

    getDropDownData: function(params){
        if(!params){
            params = { level : 1, keyword: "" }
        }

        var lData;

        $.ajax({
            url: '/bin/mycomponents/chainselect/dropdowndata',
            dataType: "json",
            type: 'GET',
            async: false,
            data: params,
            success: function(data){
                lData = data;
            }
        });

        return lData;
    },

    initComponent: function () {
        ExperienceAEM.ChainSelect.superclass.initComponent.call(this);

        var lData = this.getDropDownData();

        if(!lData){
            CQ.Ext.Msg.alert("Error","Error getting levels data or no data available");
            return;
        }

        for(var x = 1; x <= this.levels; x++){
            this.add(new CQ.Ext.form.ComboBox({
                store: new CQ.Ext.data.ArrayStore({
                    fields: ["id", "text"],
                    data: lData[x]
                }),
                mode: "local",
                triggerAction: "all",
                isFormField: false,
                level: x,
                fieldLabel: "Level " + x,
                valueField: 'id',
                displayField: 'text',
                emptyText: 'Select level ' + x,
                style: "margin-bottom:20px",
                xtype: 'combo',
                listeners:{
                    scope: this,
                    select: function(combo){
                        var keyword = combo.getValue();

                        var lowCombo = this.findBy(function(comp){
                            return comp["level"] == (combo.level + 1);
                        }, this);

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

                        lData = this.getDropDownData({ level : combo.level + 1, keyword: keyword });
                        var level = combo.level + 1;

                        do{
                            lowCombo = this.findBy(function(comp){
                                return comp["level"] == level;
                            }, this);

                            if(!lowCombo || (lowCombo.length == 0)){
                                break;
                            }

                            lowCombo = lowCombo[0];
                            lowCombo.clearValue();

                            if(lData[lowCombo.level]){
                                lowCombo.setDisabled(false);
                                lowCombo.store.loadData(lData[lowCombo.level], false);
                            }else{
                                lowCombo.setDisabled(true);
                            }

                            level = lowCombo.level + 1;
                        }while(true);
                    }
                }
            }));
        }

        this.panelValue = new CQ.Ext.form.Hidden({
            name: this.name
        });

        this.add(this.panelValue);

        var dialog = this.findParentByType('dialog');

        dialog.on('beforesubmit', function(){
            var value = this.getValue();

            if(value){
                this.panelValue.setValue(value);
            }
        },this);

        this.panelValue.on('loadcontent', function(){
            this.setValue(this.panelValue.getValue());
        },this);
    }
});

CQ.Ext.reg("chainselect", ExperienceAEM.ChainSelect);


7) Here is a sample dialog xml with chainselect widget configured ( with 3 level combos )

<?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"
    helpPath="en/cq/current/wcm/default_components.html#Text"
    title="Text"
    xtype="tabpanel">
    <items jcr:primaryType="cq:WidgetCollection">
        <tab1
            jcr:primaryType="cq:Widget"
            anchor="100%"
            title="Text"
            xtype="panel">
            <items jcr:primaryType="cq:WidgetCollection">
                <chainselect
                    jcr:primaryType="cq:Widget"
                    border="false"
                    layout="form"
                    levels="3"
                    name="./chainselect"
                    padding="10px"
                    xtype="chainselect"/>
            </items>
        </tab1>
    </items>
</jcr:root>


AEM CQ 56 - Extend RichText Editor, add new Plugin PullQuote

Goal


Add a new plugin to RichText Editor. Here we add the Pull Quote plugin for inserting text that can be formatted/styled during component rendering. Source code, Package Install and Demo Video are available for download. Please leave a comment if you find bugs.




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/rte-pull-quote

2) Create clientlib (type cq:ClientLibraryFolder/apps/rte-pull-quote/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/rte-pull-quote/clientlib/js.txt, add the following

                         js/rtepullquote.js.js

4) Create folder (nt:folder) /apps/rte-pull-quote/clientlib/js

5) Create file (type nt:file) /apps/rte-pull-quote/clientlib/js/rtepullquote.js, add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.PullQuote = {
    ADD_QUOTE_CMD : "addquote",
    REMOVE_QUOTE_CMD : "removequote",
    DEFAULT_PATTERN: "[pullquote:(align=<align>,text=<text>)]"
};

MyClientLib.PullQuote.Plugin = new Class({
    toString: "PullQuotePlugin",
    extend: CUI.rte.plugins.Plugin,
    P: MyClientLib.PullQuote,

    addQuoteUI: null,
    removeQuoteUI: null,

    getFeatures: function() {
        return [ this.P.ADD_QUOTE_CMD, this.P.REMOVE_QUOTE_CMD ];
    },

    initializeUI: function(tbGenerator) {
        var plg = CUI.rte.plugins;

        if (this.isFeatureEnabled(this.P.ADD_QUOTE_CMD)) {
            this.addQuoteUI = tbGenerator.createElement(this.P.ADD_QUOTE_CMD, this, true, "Add/Modify Pull Quote");
            tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT,this.addQuoteUI,1000);
        }

        if (this.isFeatureEnabled(this.P.REMOVE_QUOTE_CMD)) {
            this.removeQuoteUI = tbGenerator.createElement(this.P.REMOVE_QUOTE_CMD, this, true,"Remove Pull Quote");
            tbGenerator.addElement("format", plg.Plugin.SORT_FORMAT,this.removeQuoteUI,1001);
        }
    },

    execute: function(cmd, value, env) {
        if (cmd == this.P.ADD_QUOTE_CMD) {
            this.showDialog(env.editContext);
        }else {
            this.editorKernel.relayCmd(MyClientLib.PullQuote.REMOVE_QUOTE_CMD, value);
        }
    },

    showDialog: function(context) {
        var editorKernel = this.editorKernel, dm = editorKernel.getDialogManager(), pattern = this.config.pattern;

        if(!pattern){
            pattern = this.P.DEFAULT_PATTERN;
        }

        var selection = CUI.rte.Selection.createProcessingSelection(context);
        var rteText = selection.startNode.data, initValue = { align : "LEFT", text : "" };

        if(rteText){
            //this parsing logic depends on pattern, so when you add new pattern for pullquote make
            //sure you modify the following code to suit your pattern
            try{
                var start = rteText.lastIndexOf("[pullquote:", selection.startOffset);

                if( start !== -1 ){
                    var dIndex = rteText.indexOf(",");
                    initValue.align = rteText.substring(rteText.indexOf("=",start) + 1, dIndex);
                    initValue.text = rteText.substring(rteText.indexOf("=", dIndex) + 1, rteText.indexOf(")]"));
                }
            }catch(err){
                CQ.Ext.Msg.alert("Error","Error parsing text with pattern : " + pattern);
            }
        }

        var dialogConfig = {
            "jcr:primaryType": "cq:Dialog",
            title: "Pull Quote",
            modal: true,
            width: 600,
            height: 400,
            items: [ {
                xtype: "panel",
                layout: "fit",
                padding: "20px 0 0 10px",
                items: [ {
                    xtype: "panel",
                    layout: "form",
                    border: false,
                    items: [ {
                        xtype: 'radiogroup',
                        columns: 4,
                        fieldLabel: "Align",
                        items: [{
                            boxLabel: ' Left',
                            name: 'align',
                            value: 'LEFT',
                            checked: (initValue.align === "LEFT")
                        }, {
                            name: 'align',
                            boxLabel: ' Right',
                            value: 'RIGHT',
                            checked: (initValue.align === "RIGHT")
                        }, {
                            name: 'align',
                            boxLabel: ' Center',
                            value: 'CENTER',
                            checked: (initValue.align === "CENTER")
                        }, {
                            name: 'align',
                            boxLabel: ' Justify',
                            value: 'JUSTIFY',
                            checked: (initValue.align === "JUSTIFY")
                        }]
                    },{
                        xtype: "textarea",
                        height: 250,
                        name: "text",
                        fieldLabel: "Text",
                        fieldDescription: "Enter quote text",
                        anchor: "90%",
                        value: initValue.text
                    } ]
                } ]
            } ],
            ok: function() {
                var tBox = this.findByType("textarea")[0];
                var rBox = this.findByType("radiogroup")[0];

                if(!tBox.getValue()){
                    CQ.Ext.Msg.alert("Error","Enter text for quote");
                    return;
                }

                var value = {
                    text: tBox.getValue(),
                    align: rBox.getValue().value,
                    pattern: pattern
                };

                this.close();
                editorKernel.relayCmd(MyClientLib.PullQuote.ADD_QUOTE_CMD, value);
            },
            listeners: {
                show: function() {
                    editorKernel.fireUIEvent("dialogshow");
                },
                hide: function() {
                    editorKernel.fireUIEvent("dialoghide");
                }
            }
        };

        dm.show(CQ.WCM.getDialog(dialogConfig));
    },

    notifyPluginConfig: function(pluginConfig) {
        pluginConfig = pluginConfig || { };

        CUI.rte.Utils.applyDefaults(pluginConfig, {
            "tooltips": {
                "addquote": {
                    "title": "Add/Modify Pull Quote",
                    "text": "Add/Modify Pull Quote"
                },
                "removequote": {
                    "title": "Remove Pull Quote",
                    "text": "Remove Pull Quote"
                }
            }
        });

        this.config = pluginConfig;
    },

    updateState: function(selDef) {
        var rteText = selDef.selection.startNode.data;

        //this parsing logic depends on pattern, so when you add new pattern for pullquote make
        //sure you modify the following code to suit your pattern
        if(rteText && (rteText.lastIndexOf("[pullquote:", selDef.startOffset) !== -1)){
            this.removeQuoteUI.setDisabled(false);
        }else{
            this.removeQuoteUI.setDisabled(true);
        }

        this.addQuoteUI.setSelected(false);
        this.removeQuoteUI.setSelected(false);
    }
});

CUI.rte.plugins.PluginRegistry.register("pullquote", MyClientLib.PullQuote.Plugin);

MyClientLib.PullQuote.Cmd = new Class({
    toString: "PullQuote",
    extend: CUI.rte.commands.Command,

    P: MyClientLib.PullQuote,

    isCommand: function(cmdStr) {
        return (cmdStr == this.P.ADD_QUOTE_CMD) || (cmdStr == this.P.REMOVE_QUOTE_CMD);
    },

    getProcessingOptions: function() {
        var cmd = CUI.rte.commands.Command;
        return cmd.PO_SELECTION | cmd.PO_NODELIST;
    },

    addPullQuote: function(execDef){
        var value = execDef.value, selection = execDef.selection;
        var node = CUI.rte.DomProcessor.createNode(execDef.editContext, "span");

        var rteText = selection.startNode.data;
        var start = rteText ? rteText.lastIndexOf("[pullquote:", selection.startOffset) : -1;

        if( start !== -1 ){
            CUI.rte.Common.insertNode(node, selection.startNode, start);
            selection.startNode.parentNode.removeChild(selection.startNode);
        }else{
            CUI.rte.Common.insertNode(node, selection.startNode, selection.startOffset);
        }

        if(value.pattern){
            node.innerHTML = value.pattern.replace("<align>", value.align).replace("<text>", value.text);
        }else{
            node.innerHTML = "[pullquote:(align=\"" + value.align + "\",text=\"" + value.text + "\")]";
        }
    },

    removePullQuote: function(execDef){
        var selection = execDef.selection;

        var rteText = selection.startNode.data;
        var start = rteText.lastIndexOf("[pullquote:", selection.startOffset);

        if( start !== -1 ){
            selection.startNode.parentNode.removeChild(selection.startNode);
        }
    },

    execute: function(execDef) {
        if(execDef.command == this.P.ADD_QUOTE_CMD){
            this.addPullQuote(execDef);
        }else{
            this.removePullQuote(execDef);
        }
    }
});

CUI.rte.commands.CommandRegistry.register("pullquote", MyClientLib.PullQuote.Cmd);

6) #185, #243 we are registering the plugin and necessary action command. #25, #30 creates the toolbar buttons for add, remove quotes

7) Let us add the not-so-great icons and necessary css. Create folder (nt:folder) /apps/rte-pull-quote/clientlib/themes

8) Create clientlib (type cq:ClientLibraryFolder/apps/rte-pull-quote/clientlib/themes/default and set a property categories of String type to cq.widgets

9) Create file (nt:file) /apps/rte-pull-quote/clientlib/themes/default/css.txt, add the following

                    css/rtepullquote.css

10) Create folder (nt:folder) /apps/rte-pull-quote/clientlib/themes/default/css and make sure you add the icons addquote.png and removequote.png

11) Create file (nt:file) /apps/rte-pull-quote/clientlib/themes/default/css/rtepullquote.css, add the following code. RTE looks for css classes x-edit-addquote and x-edit-removequote (for the plugin toolbar buttons added namely addquote and removequote )

#CQ .x-html-editor-tb .x-edit-addquote {
    background: url(addquote.png) center no-repeat;
}

#CQ .x-html-editor-tb .x-edit-removequote {
    background: url(removequote.png) center no-repeat;
}

12) Add any text component with RichText editor and in the rtePlugins path of dialog (eg. /apps/rte-pull-quote/text/dialog/items/tab1/items/text/rtePlugins) add the pullquote node to enable pull quote plugins




AEM - Debugging Tips

        

CQ_561_CHECK_SESSIONS

1) To check the number of sessions held in memory ( read this article for more detailed analysis on dealing with unclosed sessions ), in windows, get CQ process id

Run the jmap command of jvm

                               jmap -histo:live <<pid>> | findstr CRXSessionImpl


CQ_561_GEN_THREAD_DUMPS

2) To generate and check thread dumps, either use Threads tab of CQ5 Web Console http://localhost:4502/system/console or Jstack

Run the jstack command of jvm

                       jstack <<pid>> | findstr cq

CQ_561_REMOVE_SESSION_TOKENS

3) To check active or expired sessions and remove them, access http://localhost:4502/system/console/tokenmgr, or access CRXDE Lite and remove the token nodes. eg. To check and remove the token nodes of admin user in a vanilla CQ, access http://localhost:4502/crx/de/index.jsp#/home/users/a/admin/.tokens

AEM_6_CREATE_LOGGER

4) To debug a specific problem, enabling DEBUG for error.log (logging entire CQ) is not recommended as setting the log level to DEBUG, may result in error.log growing gigantic in minutes; instead create a logger for specific package, for example to debug query builder (/bin/querybuilder.json) and log the xpath query (logged with log.debug())

Try Chrome Developer Tools AEM  Plug-In or follow the below process...

             a) Access http://localhost:4502/system/console/slinglog

             b) Create a new logger for package com.day.cq.search.impl, with level DEBUG, logging to logs\search.log



             c) Execute query builder with query http://localhost:4502/bin/querybuilder.json?path=/content/dam&p.hits=full&property=tiff:Make&property.value=Canon

             d) The xpath query logged in logs\search.log


AEM_6_DEBUG_INDEX_LOG

5)  To enable DEBUG log level for Indexing (org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate) create a new logger for oak packages, (thanks to Sammit Potade for the tip)

             a) Access http://localhost:4502/system/console/slinglog

             b) Create a new logger for packages org.apache.jackrabbit.oak.plugins.index, org.apache.jackrabbit.oak.security, org.apache.jackrabbit.oak.query, with level DEBUG, logging to logs\oak.log

             c) Create a oak index, /oak:index/styleNumber, in CRXDE Lite http://localhost:4502/crx/de

       

             d) Use ACS AEM Commons Oak Index Manager to perform index/reindex of property styleNumber - http://localhost:4502/etc/acs-commons/oak-index-manager.html 


AEM_6_CHANGE_HASH_ALGORITHM

6)  In AEM 6 by default, the password (rep:password) is stored as SHA-256



   
     To change the hashing algorithm to SHA-512, access http://localhost:4502/system/console/configMgr -> Apache Jackrabbit Oak UserConfiguration, change Hash Algorithm to SHA-512









AEM_6_CHECK_INDEXING_STATEMENTS

7)  To check indexing bottle necks from the logs

grep "org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate AsyncIndex (async) update run completed " error.log | grep -o  "[0-9]* s"| sort -n

AEM_6_CASE_INSENSITIVE_LOGIN

8)  With com.day.crx.sling.server-2.4.44.jar, Sling uses TokenBasedAuthentication of Jackrabbit and the authentication is case-sensitive by default. For case insensitive login add

-DTokenCompatMode=true to JVM_OPTS in CQ startup scripts

CREATE_JCR_NAMESPACES_ON_PUBLISH

9) To create new jcr namespaces (on publish) so that a transport user (not admin) can replicate content/assets (containing custom, project specific namespaces) from author to publish, provide jcr:namespaceManagement privilege on Repository to the user. For example, the user transporter in below picture was given jcr:namespaceManagement privilege








AEM_61_RECOMPILE_JSPS

10)  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

11)  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 Version New Version Action
1.0.22 1.0.23 Install
1.0.22 1.0.23-SNAPSHOT Install
1.0.23 1.0.23-SNAPSHOT Ignore
1.0.23-SNAPSHOT 1.0.23-SNAPSHOT Install
1.0.23-SNAPSHOT 1.0.23-r231423423 Install
1.0.23-r231423423 1.0.23-r231423423 Ignore
1.0.23-r231423423 1.0.23-r231423424 Install (last digit 4 > 3)
1.0.23-SNAPSHOT 1.0.23-T1r231423423 Install
1.0.23-T1r231423423 1.0.23-T2r231423423 Install (Patch revision number - T2r > T1r)



AEM_61_ADD_WORKFLOW_ADMINISTRATORS

12)  By default, only administrators can view all workflow instances; to 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



AEM CQ 56 - Change Tabs Order In Content Finder

Goal


Change the order of tabs in Content Finder. Here we reverse the tabs order, not particularly useful, but it shows how you can change the order in case you need to

From the Product



Extended to Reverse



References


1) To extend page tab and add additional filters check this post

2)  For hiding unused tabs in content finder check this post

3) For showing additional image metadata in content finder, check this post

Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/reversecftabs

2) Create node /apps/reversecftabs/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/reversecftabs/clientlib/js.txt and add

                       reversetabs.js

4) Create file (nt:file) /apps/reversecftabs/clientlib/reversetabs.js and add the following code

CQ.Ext.ns("MyClientLib");

MyClientLib.ContentFinder = CQ.Ext.extend(CQ.wcm.ContentFinder, {
    getVisibleTabs: function(path) {
        var tabs = MyClientLib.ContentFinder.superclass.getVisibleTabs.call(this, path);

        //order is based on ranking, so change them
        $.each(tabs, function(index, tab){
            tab.ranking = tabs.length - index;
        });

        return tabs;
    }
});

CQ.Ext.reg("contentfinder", MyClientLib.ContentFinder);


AEM CQ 56 - Open Sidekick Page Properties Dialog Programmatically

Goal


Open the Page properties dialog programmatically and not from Sidekick. Check the demo

Solution


In your page, add a button click listener with below logic

<input type=button onClick="openPagePropertiesDialog()" value="Open Page Properties"/>

<script>
    function openPagePropertiesDialog(){
        var sk = CQ.WCM.getSidekick();

        if(!sk){
            alert("Sidekick not available, is the sidekick fully loaded on page?");
            return;
        }

        var pageTab = sk.findById("cq-sk-tab-PAGE");

        var openDialog = function(){
            var pagePropsButton = pageTab.findBy(function(comp){
                return comp["text"] == "Page Properties...";
            }, pageTab);

            pagePropsButton[0].handler.call(sk);
        };

        //if sidekick is not expanded, expand it and later open the dialog
        if(!pageTab){
            var toggle = sk.tools["toggle"];
            toggle.dom.click();

            var SK_INTERVAL = setInterval(function(){
                pageTab = sk.findById("cq-sk-tab-PAGE");

                if(pageTab){
                    clearInterval(SK_INTERVAL);
                    openDialog();
                }
            }, 250);
        }else{
            openDialog();
        }
    }
</script>