AEM 61 - Touch UI Default Image (Placeholder) in Granite File Upload Widget

Goal


Add default (placeholder) image capability to components with Granite File Upload widgets granite/ui/components/foundation/form/fileupload in cq:dialog. In the demo, a custom component /apps/touchui-fileupload-default-image/sample-image-component with two file upload widgets in cq:dialog are configured with placeholder attribute value /content/dam/Paintings/Placeholder.png. When a new component editable is created using drag/drop on page, the file upload widget fileReferenceParameter value is automatically filled with the placeholder file path. When a new component is added, user will always see placeholder image first, before any image can be dropped or uploaded in widget. The placeholder image may provide some instructions on what type of images can be used for the widget (eg. the placeholder may say upload PNGs only)

Demo | Package Install


Placeholder Image added on Component Drop




Placeholder configuration in cq:dialog



Placeholder path in CRX, added on Component Drop




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-fileupload-default-image

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-fileupload-default-image/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-fileupload-default-image/clientlib/js.txt, add the following

                         default-image.js

4) Create file ( type nt:file ) /apps/touchui-fileupload-default-image/clientlib/default-image.js, add the following code

(function ($document, gAuthor) {
    var COMPONENT = "touchui-fileupload-default-image/sample-image-component",
        FILE_UPLOAD_WIDGET = "granite/ui/components/foundation/form/fileupload",
        DROP_TARGET_ENABLED_CLASS = "js-cq-droptarget--enabled",
        FILE_REF_PARAM = "fileReferenceParameter",
        PLACEHOLDER_PARAM = "placeholder";

    $document.on('cq-inspectable-added', addPlaceholder);

    $document.on('mouseup', setNewDropFlag);

    var newComponentDrop = false;

    function setNewDropFlag(event){
        var LM = gAuthor.layerManager;

        if (LM.getCurrentLayer() != "Edit") {
            return;
        }

        var DC = gAuthor.ui.dropController,
            generalIH = DC._interactionHandler.general;

        if (!generalIH._hasStarted || !$(event.target).hasClass(DROP_TARGET_ENABLED_CLASS)) {
            return;
        }

        newComponentDrop = true;
    }

    function addPlaceholder(event){
        var LM = gAuthor.layerManager;

        if ( (LM.getCurrentLayer() != "Edit") || !newComponentDrop) {
            return;
        }

        newComponentDrop = false;

        var editable = event.inspectable;

        if(editable.type !== COMPONENT){
            return;
        }

        $.ajax(editable.config.dialog + ".infinity.json").done(postPlaceholder);

        function postPlaceholder(data){
            var fileRefs = addFileRefs(data, {});

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

            $.ajax({
                type : 'POST',
                url : editable.path,
                data  : fileRefs
            }).done(function(){
                editable.refresh();
            })
        }

        function addFileRefs(data, fileRefs){
            if(_.isEmpty(data)){
                return fileRefs;
            }

            _.each(data, function(value, key){
                if(_.isObject(value)){
                    addFileRefs(value, fileRefs);
                }

                if( (key != "sling:resourceType") || (value != FILE_UPLOAD_WIDGET)){
                    return;
                }

                if(!data[FILE_REF_PARAM] || !data[PLACEHOLDER_PARAM]){
                    return;
                }

                fileRefs[data[FILE_REF_PARAM]] = data[PLACEHOLDER_PARAM];
            }, this);

            return fileRefs;
        }
    }
})($(document), Granite.author);

AEM 61 - Touch UI Image Multifield

Goal


Create  a Touch UI Composite Multifield configuration supporting Images, widgets of type granite/ui/components/foundation/form/fileupload

For Classic UI Image Multifield check this post

For AEM 62 Image Multifield check this post

Hotfix 6670 must be installed for this widget extension to work

For a slideshow component check this post

For AEM 6440 check this post

Demo | Package Install


Bug Fixes

Support for File Upload in Multifield - Demo | Package Install

Select and Checkbox fixes - Demo | Package Install

Image Multifield for 61 SP1 (no hotfixes necessary) - Demo | Package Install


Image Mulitifield



Nodes in CRX



Dialog



Dialog XML

#49 makes the widget a composite multifield by adding empty valued flag eaem-nested
#78 fileReferenceParameter for storing image path

<?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="Touch UI Image Multi Field"
    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/tabs"
            type="nav"/>
        <items jcr:primaryType="nt:unstructured">
            <gallery
                jcr:primaryType="nt:unstructured"
                jcr:title="Gallery"
                sling:resourceType="granite/ui/components/foundation/section">
                <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="India 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">
                                            <gallery
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                fieldDescription="Enter Gallery Name"
                                                fieldLabel="Gallery"
                                                name="./gallery"/>
                                            <artist
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/foundation/form/multifield"
                                                class="full-width"
                                                eaem-nested=""
                                                fieldDescription="Click '+' to add a Artist"
                                                fieldLabel="URLs">
                                                <field
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/foundation/form/fieldset"
                                                    name="./artists">
                                                    <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">
                                                                <artist
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/textfield"
                                                                    fieldDescription="Enter Artist Name"
                                                                    fieldLabel="Artist"
                                                                    name="./artist"/>
                                                                <painting
                                                                    jcr:primaryType="nt:unstructured"
                                                                    sling:resourceType="granite/ui/components/foundation/form/fileupload"
                                                                    autoStart="{Boolean}false"
                                                                    class="cq-droptarget"
                                                                    fieldLabel="Painting"
                                                                    fileNameParameter="./paintingName"
                                                                    fileReferenceParameter="./paintingRef"
                                                                    mimeTypes="[image]"
                                                                    multiple="{Boolean}false"
                                                                    name="./painting"
                                                                    title="Upload Image"
                                                                    uploadUrl="${suffix.path}"
                                                                    useHTML5="{Boolean}true"/>
                                                            </items>
                                                        </column>
                                                    </items>
                                                </field>
                                            </artist>
                                        </items>
                                    </column>
                                </items>
                            </fieldset>
                        </items>
                    </column>
                </items>
            </gallery>
        </items>
    </content>
</jcr:root>


Component Rendering



Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-image-multifield/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-image-multifield/clientlib/js.txt, add the following

                         image-multifield.js

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

(function () {
    var DATA_EAEM_NESTED = "data-eaem-nested",
        CFFW = ".coral-Form-fieldwrapper",
        THUMBNAIL_IMG_CLASS = "cq-FileUpload-thumbnail-img",
        SEP_SUFFIX = "-",
        SEL_FILE_UPLOAD = ".coral-FileUpload",
        SEL_FILE_REFERENCE = ".cq-FileUpload-filereference",
        SEL_FILE_NAME = ".cq-FileUpload-filename",
        SEL_FILE_MOVEFROM = ".cq-FileUpload-filemovefrom";

    function getStringBeforeAtSign(str){
        if(_.isEmpty(str)){
            return str;
        }

        if(str.indexOf("@") != -1){
            str = str.substring(0, str.indexOf("@"));
        }

        return str;
    }

    function getStringAfterAtSign(str){
        if(_.isEmpty(str)){
            return str;
        }

        return (str.indexOf("@") != -1) ? str.substring(str.indexOf("@")) : "";
    }

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

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

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

        return str.substr(0, str.lastIndexOf("/"));
    }

    function removeFirstDot(str){
        if(str.indexOf(".") != 0){
            return str;
        }

        return str.substr(1);
    }

    function modifyJcrContent(url){
        return url.replace(new RegExp("^" + Granite.HTTP.getContextPath()), "")
                .replace("_jcr_content", "jcr:content");
    }

    /**
     * Removes multifield number suffix and returns just the fileRefName
     * Input: paintingRef-1, Output: paintingRef
     *
     * @param fileRefName
     * @returns {*}
     */
    function getJustName(fileRefName){
        if(!fileRefName || (fileRefName.indexOf(SEP_SUFFIX) == -1)){
            return fileRefName;
        }

        var value = fileRefName.substring(0, fileRefName.lastIndexOf(SEP_SUFFIX));

        if(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1 == fileRefName.length){
            return value;
        }

        return value + fileRefName.substring(fileRefName.lastIndexOf(SEP_SUFFIX) + SEP_SUFFIX.length + 1);
    }

    function getMultiFieldNames($multifields){
        var mNames = {}, mName;

        $multifields.each(function (i, multifield) {
            mName = $(multifield).children("[name$='@Delete']").attr("name");
            mName = mName.substring(0, mName.indexOf("@"));
            mName = mName.substring(2);
            mNames[mName] = $(multifield);
        });

        return mNames;
    }

    function buildMultiField(data, $multifield, mName){
        if(_.isEmpty(mName) || _.isEmpty(data)){
            return;
        }

        _.each(data, function(value, key){
            if(key == "jcr:primaryType"){
                return;
            }

            $multifield.find(".js-coral-Multifield-add").click();

            _.each(value, function(fValue, fKey){
                if(fKey == "jcr:primaryType" || _.isObject(fValue)){
                    return;
                }

                var $field = $multifield.find("[name='./" + fKey + "']").last();

                if(_.isEmpty($field)){
                    return;
                }

                $field.val(fValue);
            });
        });
    }

    function buildImageField($multifield, mName){
        $multifield.find(".coral-FileUpload:last").each(function () {
            var $element = $(this), widget = $element.data("fileUpload"),
                resourceURL = $element.parents("form.cq-dialog").attr("action"),
                counter = $multifield.find(SEL_FILE_UPLOAD).length;

            if (!widget) {
                return;
            }

            var fuf = new Granite.FileUploadField(widget, resourceURL);

            addThumbnail(fuf, mName, counter);
        });
    }

    function addThumbnail(imageField, mName, counter){
        var $element = imageField.widget.$element,
            $thumbnail = $element.find("." + THUMBNAIL_IMG_CLASS),
            thumbnailDom;

        $thumbnail.empty();

        $.ajax({
            url: imageField.resourceURL + ".2.json",
            cache: false
        }).done(handler);

        function handler(data){
            var fName = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileName)),
                fRef = getJustName(getStringAfterLastSlash(imageField.fieldNames.fileReference));

            if(isFileNotFilled(data, counter, fRef)){
                return;
            }

            var fileName = data[mName][counter][fName],
                fileRef = data[mName][counter][fRef];

            if (fileRef) {
                if (imageField._isImageMimeType(fileRef)) {
                    thumbnailDom = imageField._createImageThumbnailDom(fileRef);
                } else {
                    thumbnailDom = $("<p>" + fileRef + "</p>");
                }
            }

            if (!thumbnailDom) {
                return;
            }

            $element.addClass("is-filled");
            $thumbnail.append(thumbnailDom);

            var $fileName = $element.find("[name=\"" + imageField.fieldNames.fileName + "\"]"),
                $fileRef = $element.find("[name=\"" + imageField.fieldNames.fileReference + "\"]");

            $fileRef.val(fileRef);
            $fileName.val(fileName);
        }

        function isFileNotFilled(data, counter, fRef){
            return _.isEmpty(data[mName])
                    || _.isEmpty(data[mName][counter])
                    || _.isEmpty(data[mName][counter][fRef])
        }
    }

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

            if(_.isEmpty($multifields)){
                return;
            }

            var mNames = getMultiFieldNames($multifields),
                $form = $(".cq-dialog"),
                actionUrl = $form.attr("action") + ".infinity.json";

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

            function postProcess(data){
                _.each(mNames, function($multifield, mName){
                    $multifield.on("click", ".js-coral-Multifield-add", function () {
                        buildImageField($multifield, mName);
                    });

                    buildMultiField(data[mName], $multifield, mName);
                });
            }
        });
    }

    function collectImageFields($form, $fieldSet, counter){
        var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
            return $(ele).find(SEL_FILE_UPLOAD).length == 0;
        });

        $fields.each(function (j, field) {
            var $field = $(field),
                $widget = $field.find(SEL_FILE_UPLOAD).data("fileUpload");

            if(!$widget){
                return;
            }

            var prefix = $fieldSet.data("name") + "/" + (counter + 1) + "/",

                $fileRef = $widget.$element.find(SEL_FILE_REFERENCE),
                refPath = prefix + getJustName($fileRef.attr("name")),

                $fileName = $widget.$element.find(SEL_FILE_NAME),
                namePath = prefix + getJustName($fileName.attr("name")),

                $fileMoveRef = $widget.$element.find(SEL_FILE_MOVEFROM),
                moveSuffix =   $widget.inputElement.attr("name") + "/" + new Date().getTime()
                                        + SEP_SUFFIX + $fileName.val(),
                moveFromPath =  moveSuffix + "@MoveFrom";

            $('<input />').attr('type', 'hidden').attr('name', refPath)
                .attr('value', $fileRef.val() || ($form.attr("action") + removeFirstDot(moveSuffix)))
                .appendTo($form);

            $('<input />').attr('type', 'hidden').attr('name', namePath)
                .attr('value', $fileName.val()).appendTo($form);

            $('<input />').attr('type', 'hidden').attr('name', moveFromPath)
                .attr('value', modifyJcrContent($fileMoveRef.val())).appendTo($form);

            $field.remove();
        });
    }

    function collectNonImageFields($form, $fieldSet, counter){
        var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
            return $(ele).find(SEL_FILE_UPLOAD).length > 0;
        });

        $fields.each(function (j, field) {
            fillValue($form, $fieldSet.data("name"), $(field).find("[name]"), (counter + 1));
        });
    }

    function fillValue($form, fieldSetName, $field, counter){
        var name = $field.attr("name");

        if (!name) {
            return;
        }

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

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

        $('<input />').attr('type', 'hidden')
            .attr('name', fieldSetName + "/" + counter + "/" + name)
            .attr('value', $field.val())
            .appendTo($form);
    }

    //collect data from widgets in multifield and POST them to CRX
    function collectDataFromFields(){
        $(document).on("click", ".cq-dialog-submit", function () {
            var $multifields = $("[" + DATA_EAEM_NESTED + "]");

            if(_.isEmpty($multifields)){
                return;
            }

            var $form = $(this).closest("form.foundation-form"),
                $fieldSets;

            $multifields.each(function(i, multifield){
                $fieldSets = $(multifield).find("[class='coral-Form-fieldset']");

                $fieldSets.each(function (counter, fieldSet) {
                    collectNonImageFields($form, $(fieldSet), counter);

                    collectImageFields($form, $(fieldSet), counter);
                });
            });
        });
    }

    function overrideGranite_computeFieldNames(){
        var prototype = Granite.FileUploadField.prototype,
            ootbFunc = prototype._computeFieldNames;

        prototype._computeFieldNames = function(){
            ootbFunc.call(this);

            var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]");

            if(_.isEmpty($imageMulti)){
                return;
            }

            var fieldNames = {},
                fileFieldName = $imageMulti.find("input[type=file]").attr("name"),
                $fieldSet = $imageMulti.find(SEL_FILE_UPLOAD).closest("[class='coral-Form-fieldset']"),
                counter = $imageMulti.find(SEL_FILE_UPLOAD).length;

            _.each(this.fieldNames, function(value, key){
                if(value.indexOf("./jcr:") == 0){
                    fieldNames[key] = value;
                }else if(key == "tempFileName" || key == "tempFileDelete"){
                    value = value.substring(0, value.indexOf(".sftmp")) + getStringAfterAtSign(value);
                    fieldNames[key] = fileFieldName + removeFirstDot(getStringBeforeAtSign(value))
                                        + SEP_SUFFIX + counter + ".sftmp" + getStringAfterAtSign(value);
                }else{
                    fieldNames[key] = getStringBeforeAtSign(value) + SEP_SUFFIX
                                                    + counter + getStringAfterAtSign(value);
                }
            });

            this.fieldNames = fieldNames;

            this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath);
            this._tempFilePath = getStringBeforeLastSlash(this._tempFilePath) + removeFirstDot(fieldNames.tempFileName);
        }
    }

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

    overrideGranite_computeFieldNames();
})();


AEM 61 - Offloading DAM Upload Asset Workflow Process

Goal


Create a Master/Worker Author AEM Topology for offloading the asset upload post process, to a worker instance. So the worker instance is specifically configured to execute DAM Update Asset Workflow steps on assets uploaded to master instance.

1) Master is configured with DAM Update Asset Offloading Workflow
2) Users never see the worker author and always upload assets to master author
3) When assets are uploaded, a replication agent on master sends the asset package to worker
4) Worker executes DAM Update Asset Workflow on asset
5) Using reverse replication, the package with processed asset and renditions is replicated to master from worker

Clearly, processing too many heavy DAM assets in the author AEM instance (also used for authoring web pages) can drastically bring down performance, so offloading DAM Update Asset Workflow in such cases is considered wise....

Check adobe documentation for more information on Offloading Jobs & Sling Discovery features. The following steps are for local setup, so offloading replication agents use admin credentials and not a dedicated replication user.

Demo

Solution


1) Assuming there are two AEM author instances running on two separate machines (windows & mac) in same network...

                         Host                      IP Address                  OS                        Type                        URL

                         nalabotu-w7          192.168.0.3                  Windows              Master                       http://nalabotu-w7:4502/welcome
                         nalabotu-osx         192.168.0.8                   Macintosh            Worker                      http://nalabotu-osx:4502/welcome
                       
2) The first step is to add worker (nalabout-osx) in master (nalabotu-w7) topology. Here is the topology configuration on master before adding a worker; one instance exists with sling id ab3f05cd-1669-4c8a-b538-e24f6b37cfdf

                            http://nalabotu-w7:4502/system/console/topology

3) Access the worker topology management console. It has one instance with sling id da632281-8b9b-4aad-bfe9-89abdff4d471

                            http://nalabotu-osx:4502/system/console/topology



4) In the worker topology, configure sling discovery service with topology connector url (the toplogy connector running on master instance)

                            http://nalabotu-osx:4502/system/console/configMgr/org.apache.sling.discovery.impl.Config

                            Topology Connector URL: http://nalabotu-w7:4502/libs/sling/topology/connector



5) Add the worker hostname and ip address in master discovery service white list configuration.

                            http://nalabotu-w7:4502/system/console/configMgr/org.apache.sling.discovery.impl.Config

                            Topology Connector Whitelist: nalabotu-osx, 192.168.0.8



6) The topology after successfully adding a worker


7) The system should have created necessary replication agents on master. Access the replication agents console on master author. The replication agent responsible for sending asset input payload to worker is named offloading_<worker-sling-id>, and the reverse replication agent for getting processed package back into master is named offloading_reverse_<worker-sling-id>

                            http://nalabotu-w7:4502/etc/replication/agents.author.html



8) Granite offloading console shows the consumers for each topic, when a worker joins topology the consumers for offloading topic com/adobe/granite/workflow/offloading are available and active on both master and worker. To avoid master also processing the uploaded asset (only worker should), disable the consumer on master. In the screenshot below consumer on master (nalabotu-w7 192.168.0.3) is disabled, shown with red button

                            http://nalabotu-w7:4502/libs/granite/offloading/content/view.html



9) The next step is to configure DAM Update Asset Workflow Launchers on Master (nalabotu-w7). This step is to initiate, offloading workflow and not ootb regular update asset. Access Launcher console and change DAM Update Asset to DAM Update Asset Offloading workflow

                            http://nalabotu-w7:4502/libs/cq/workflow/content/console.html


                            Before Update



                            After Updating to DAM Update Asset Offloading



10) Access worker Launcher console and disable the DAM Update Asset Workflow. The assets are not uploaded to worker, but offloaded from master; so DAM Update Asset Workflow should remain disabled on worker



11) Upload any assets to master http://nalabotu-w7:4502/assets.html and the DAM Update Asset process happens on worker http://nalabotu-osx:4502/assets.html. Check the replication agent log



AEM 61 - Random JCR Queries

JCR-SQL2


To execute a JCR query, try using CRXDE Lite http://localhost:4502/crx/de/index.jsp -> Tools -> Query



More queries - https://gist.github.com/floriankraft/8b3720464318cd5cd9e2



1) Get global collections

SELECT * FROM [nt:unstructured] WHERE [sling:resourceType] = 'dam/collection'


2) Get Project specific collections

SELECT * FROM [nt:unstructured] WHERE [sling:resourceType] = 'cq/gui/components/projects/admin/card/projectcard' and NAME() = '150609_belk_fisharmvntbb';


3) Get nodes (DAM Assets) with specified name

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd'


4) Get nodes (DAM Assets) with specified name, order by path

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd' ORDER BY 'jcr:path'


5) Get nodes (DAM Assets) with specified name, order by last modified date descending

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd' ORDER BY 'jcr:content/jcr:lastModified' desc


6) Find node with a specific property value. For example to get the node with jcr:uuid value '9074d289-faae-40b2-9400-216ed4d0fa06' (here the node type is unknown)

SELECT * FROM [nt:base] WHERE [jcr:uuid] = '9074d289-faae-40b2-9400-216ed4d0fa06'

XPath

/jcr:root/content/dam/texas//element(*, dam:Asset)[(@jcr:uuid = 'e164529c-2ea2-4925-9ad5-0c443ec57ee3')]

7) Get DAM assets in a specific path. To get the assets in folder and subfolders of /content/dam/Product/Silhouettes/Accessories/Bands/Headband

SELECT * FROM [dam:Asset] WHERE ISDESCENDANTNODE("/content/dam/Product/Silhouettes/Accessories/Bands/Headband")

8) Get the items in a user(s) inbox

SELECT * FROM [granite:InboxItem] AS s where (assignee = 'admin' or assignee = 'author') and status = "ACTIVE"

9) Get all pages with word "triangle" in components

SELECT * from [cq:Page] AS t WHERE ISDESCENDANTNODE([/content/geometrixx]) AND contains(t.*, 'triangle')

10) Get all "rep: policy"  nodes that are not of type "rep:ACL"

select * from [nt:base] as t where name(t) = 'rep:policy' AND t.[jcr:primaryType] NOT LIKE 'rep:ACL'

11) Find the config node of a sling service in CRX

select * from [sling:OsgiConfig] as s where NAME(s) = 'org.apache.sling.commons.mime.internal.MimeTypeServiceImpl'

12) Find assets (dam:Asset) with a specific metadata property containing any value (not null)

select * from [dam:Asset] as d where d.[jcr:content/metadata/uaDIO:division] IS NOT NULL

13) Find nodes modified after a specified date (This query is not functioning as expected in oak, may be because jcr:lastModified is not always being stored as date)

SELECT * FROM [nt:base]  WHERE[jcr:lastModified] > cast('2015-07-25T00:00:00.000Z' as date) order by [jcr:lastModified] desc

14) Find nodes with property cq:tags containing values experience-aem:english or stockphotography:business/businesspeople (cq:tags is a multi-valued property, so query checks if each value is equal to the ones specified in query)

SELECT * from [nt:base] AS t WHERE ISDESCENDANTNODE([/content]) AND ( t.[cq:tags] = 'experience-aem:english' 
OR t.[cq:tags] = 'stockphotography:business/businesspeople')

15) Sample union query, returning dam assets with metadata property eaem:option1='One' or property eaem:option2='Two'

select [jcr:path], [jcr:score], * from [dam:Asset] as a where isdescendantnode(a, '/content/dam') and [jcr:content/metadata/eaem:option1] = 'One'
union select [jcr:path], [jcr:score], * from [dam:Asset] as a where isdescendantnode(a, '/content/dam') and [jcr:content/metadata/eaem:option2] = 'Two'

16) Find pages under a path eg. /content/mobileapps/ua-mens-running/collections, where name of a page is like something eg. global_FW16_Running_Apparel_

SELECT * FROM [cq:Page] WHERE ISDESCENDANTNODE('/content/mobileapps/ua-mens-running/collections') AND NAME([cq:Page]) like 'global_FW16_Running_Apparel_%'

17) Finding the resource with a sling vanity path eg. /assetdetails

SELECT * FROM [sling:VanityPath] WHERE [sling:vanityPath] = '/assetdetails'

18) Get the list of available otb asset metadata fields in 64

SELECT * FROM [nt:unstructured]  AS t WHERE ISDESCENDANTNODE('/libs/dam/content/schemaeditors') AND t.[name] like './%'

19) Check if a thumbnail rendition exists

select * from [dam:Asset] as d where d.[jcr:content/renditions/cq5dam.thumbnail.48.48.png/jcr:primaryType] not like 'nt:file'

20) Find assets with a tag

/jcr:root/content/dam/psds//element(*, dam:Asset)[ jcr:like(jcr:content/metadata/@cq:tags,  'we-retail:activity') ] order by @jcr:score

21) Find asset paths used in a page

SELECT * from [nt:unstructured] AS t WHERE ISDESCENDANTNODE([/content/eaem/us/en/home/jcr:content]) AND t.[fileReference] LIKE '%/content/dam/eaem/%' 
UNION 
SELECT * from [nt:unstructured] AS t WHERE ISDESCENDANTNODE([/content/eaem/us/en/home/jcr:content]) AND t.[src] LIKE '%/content/dam/eaem/%'

22) XPath query for checking if the name matches....

/jcr:root/content/dam//element(*, dam:Asset)[(fn:name()='256551_E_DGT_Invested_Beef_Print ad_3-5x8.indd')]

/jcr:root/content/dam//element(*, dam:Asset)[jcr:like(fn:name(),'256551_E_DGT_Invested_Beef_Print ad_3-5x8.indd')]

23) Find all published  content on that particular day 

select [jcr:path], [jcr:score], * from [cq:Page] as a where [jcr:content/cq:lastReplicated] > cast('2021-12-10T00:01:00.000Z' as date) and [jcr:content/cq:lastReplicated] < cast('2021-12-11T23:59:00.000Z' as date) and [jcr:content/cq:lastReplicationAction] = 'Activate' and isdescendantnode(a, '/content')

24) 
Find assets with missing metadata node

select * from [dam:Asset] as d where isdescendantnode('/content/dam/test') AND d.[jcr:content/metadata/jcr:primaryType] not like 'nt:unstructured'

25) 
Find assets modified within a date range eg. 2025-04-28 to 2025-05-14

select [jcr:path], [jcr:score], * from [dam:Asset] as a where [jcr:content/jcr:lastModified] > cast('2025-04-28T00:01:00.000Z' as date) and [jcr:content/jcr:lastModified] < cast('2025-05-14T23:59:00.000Z' as date) and isdescendantnode(a, '/content/dam/eaem')
26)

AEM - Continuous Integration with Jenkins

Goal


For Source Code Management using GIT (Bitbucket) check this post

Jenkins is a continuous integration tool (CI) for automating builds. In simple terms, developers in a AEM project code a feature or bugfix, test on their local instances, commit/push to a central SVN or GIT repo; continuous integration tools like Jenkins kick off, build packages and deploy to some common AEM test/integration servers. Quality team can then test the feature/bug fix on AEM integration server

In a nutshell...

1) Developer starts working on a feature/bug-fix, marks the story in JIRA as In Progress
2) Tests the code on local AEM
3) Commits/Pushes change to the SVN/GIT repo. For source code management using GIT check this post
4) A configured Jenkins Hook in GIT can kickoff the build, deploy packages to AEM Integration Server. When too many changes are being pushed to the repo, admin may choose to manually start builds through Jenkins console (Jobs), to refresh integration environments.
5) Developer moves the story to QA
6) Quality team picks up the story and tests code changes on Integration Server.

Build Demo


Install Jenkins

1) Get Jenkins for Windows here

2) Run install with default settings. For more refined steps check this link

2) When completed, service Jenkins is available and a browser window opens up with url http://localhost:8080/


Configure Jenkins Global Security

1) After installation, by default, no authentication is required for accessing jenkins console. So it allows anyone create a job, right away

2) To secure Jenkins, enable Global Security (Manage Jenkins -> Configure Global Security) http://localhost:8080/configureSecurity/



3)  A Jenkins internal database of users can be created by selecting Jenkins’ own user database option or connect to organization LDAP. In the following example, Jenkins was connected to ldap on localhost:389 (a sample OpenLDAP database). So any logged-in user can modify the configuration, create jobs etc, a more fine grained access control can be set by selecting Matrix-based security



4) Two sample users eaem, nalabotu were created in local OpenLDAP database



JDK, GIT, Maven Configuration

1) Access the configuration screen, Manage Jenkins -> Configure System (http://localhost:8080/configure)


2) Configure the JDK used for compiling sources




3) Configure the GIT plugin to download sources from remote repository. For this post, use sample repo experience-aem-intranet created in this post


4) Add the path to GIT executable on file system, used for checking out source code



5) Configure MAVEN install, required for running any typical AEM project build



6) Restart Jenkins service


Creating Build Jobs

1) Create new job (If not already logged-in, login as user, say eaem)


2) Enter name experience-aem-intranet-portal and select the project type Maven


3) Configure the GIT repository url and credentials. The Branches to build specifies which branch of the repo should be downloaded and compiled, here its develop






4) Specify the relative path of pom.xml; the goal autoInstallPackage builds, installs packages to provided CQ instance, here its localhost

                 clean install -X -P autoInstallPackage -Dcrx.host=localhost -Dcrx.port=4502 -Dcrx.user=admin -Dcrx.password=admin -Dvault.timeout=30

5) Run the build by clicking Build Now


6) Build creates a workspace with sources downloaded from remote GIT repo


7) Packages are Installed on AEM running on localhost:4502


8) If the GIT repo can communicate with Jenkins, a web hook can be configured in GIT to trigger a build automatically when there is a commit on build branch say develop


AEM 61 - Classic UI Nested Composite Multifield Panel

Goal


Create a Classic UI Nested Composite Multifield Panel. The logic for nested composite multifield and composite multifield is same; this post just has a copy of composite multifield code with additional dialog configuration required for nested multifield

For Touch UI Nested Composite Multifieldcheck this post

Demo | Package Install


Nested Composite Multifield



Values Stored in CRX as JSON



Dialog



Dialog as XML

#27 specifies the xtype multi-field-panel, for parent composite multifield, #60 the child composite multifield

<?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">
    <items
        jcr:primaryType="cq:Widget"
        xtype="tabpanel">
        <items jcr:primaryType="cq:WidgetCollection">
            <tab1
                jcr:primaryType="cq:Panel"
                title="Add">
                <items jcr:primaryType="cq:WidgetCollection">
                    <map
                        jcr:primaryType="cq:Widget"
                        hideLabel="false"
                        name="./map"
                        title="Map"
                        xtype="multifield">
                        <fieldConfig
                            jcr:primaryType="cq:Widget"
                            border="true"
                            hideLabel="true"
                            layout="form"
                            padding="10px"
                            width="1000"
                            xtype="multi-field-panel">
                            <items jcr:primaryType="cq:WidgetCollection">
                                <product-year-value
                                    jcr:primaryType="cq:Widget"
                                    dName="year"
                                    fieldLabel="Year"
                                    width="60"
                                    xtype="textfield"/>
                                <product-price-value
                                    jcr:primaryType="cq:Widget"
                                    dName="price"
                                    fieldLabel="Price"
                                    width="60"
                                    xtype="textfield"/>
                                <product-version-value
                                    jcr:primaryType="cq:Widget"
                                    dName="version"
                                    fieldLabel="Path to Version"
                                    xtype="pathfield"/>
                                <product-region-multifield
                                    jcr:primaryType="cq:Widget"
                                    dName="region"
                                    fieldLabel="Region"
                                    hideLabel="false"
                                    title="Add Regions"
                                    xtype="multifield">
                                    <fieldConfig
                                        jcr:primaryType="cq:Widget"
                                        border="true"
                                        hideLabel="true"
                                        layout="form"
                                        padding="10px"
                                        width="1000"
                                        xtype="multi-field-panel">
                                        <items jcr:primaryType="cq:WidgetCollection">
                                            <product-country
                                                jcr:primaryType="cq:Widget"
                                                dName="country"
                                                fieldLabel="Country"
                                                width="60"
                                                xtype="textfield"/>
                                            <product-state
                                                jcr:primaryType="cq:Widget"
                                                dName="state"
                                                fieldLabel="State"
                                                width="60"
                                                xtype="textfield"/>
                                        </items>
                                    </fieldConfig>
                                </product-region-multifield>
                            </items>
                        </fieldConfig>
                    </map>
                </items>
            </tab1>
        </items>
    </items>
</jcr:root>


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-nested-multi-field-panel

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-nested-multi-field-panel/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-nested-multi-field-panel/clientlib/js.txt, add the following

                         multi-field.js

4) Create file ( type nt:file ) /apps/classic-ui-nested-multi-field-panel/clientlib/multi-field.js, add the following code

CQ.Ext.ns("ExperienceAEM");

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

    constructor: function(config){
        config = config || {};
        ExperienceAEM.MultiFieldPanel.superclass.constructor.call(this, config);
    },

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

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

    },

    afterRender : function(){
        ExperienceAEM.MultiFieldPanel.superclass.afterRender.call(this);

        this.items.each(function(){
            if(!this.contentBasedOptionsURL
                    || this.contentBasedOptionsURL.indexOf(CQ.form.Selection.PATH_PLACEHOLDER) < 0){
                return;
            }

            this.processPath(this.findParentByType('dialog').path);
        })
    },

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

        this.items.each(function(i){
            if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){
                return;
            }

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

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

    setValue: function (value) {
        this.panelValue.setValue(value);

        var pData = JSON.parse(value);

        this.items.each(function(i){
            if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){
                return;
            }

            i.setValue(pData[i.dName]);
        });
    },

    validate: function(){
        return true;
    },

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

CQ.Ext.reg("multi-field-panel", ExperienceAEM.MultiFieldPanel);

AEM 61 - Publishing MCM Campaign Experiences With Page Publish in Workflow Step

Goal


Publish MCM (Marketing Campaign Manager) Campaign, Adobe Target Experiences with Page Publish in a Workflow. When author activates page using Sidekick Activate Page button, any references, including campaigns are shown for selection; author can optionally select campaigns to be published with page. If the page is published in workflow step, any associated campaigns, target experiences are not published automatically. This post is on adding a Process Step in workflow to publish the referenced Campaigns and associated Adobe Target Experiences

Most of the code pieces were collected from various sources. Thanks to the unknown coders

Demo | Package Install | Source Code


Solution


1) Create an OSGI service apps.experienceaem.campaign.PublishCampaignStep extending com.day.cq.workflow.exec.WorkflowProcess, add the following code...

package apps.experienceaem.campaign;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.ReplicationStatus;
import com.day.cq.replication.Replicator;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.reference.*;
import com.day.cq.wcm.api.reference.Reference;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.HistoryItem;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.day.cq.workflow.model.WorkflowNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.apache.felix.scr.annotations.ReferenceCardinality.OPTIONAL_MULTIPLE;
import static org.apache.felix.scr.annotations.ReferencePolicy.DYNAMIC;

@Component
@Service(WorkflowProcess.class)
@Property(name = "process.label", value = "Experience AEM Publish Campaign for Page")
public class PublishCampaignStep implements WorkflowProcess {
    private final Logger log = LoggerFactory.getLogger(PublishCampaignStep.class);

    private static final String REPLICATE_AS_PARTICIPANT = "replicateAsParticipant";
    private static final String PROCESS_ARGS = "PROCESS_ARGS";

    private static final String PN_OFFERPATH = "offerPath";
    private static final String REF_CAMPAIGN = "campaign";
    private static final String LOCATION = "location";


    private static final String ARP = "com.day.cq.dam.commons.util.impl.AssetReferenceProvider";

    @org.apache.felix.scr.annotations.Reference(policy = ReferencePolicy.STATIC)
    private JcrResourceResolverFactory factory;

    @org.apache.felix.scr.annotations.Reference
    private Replicator replicator;

    @org.apache.felix.scr.annotations.Reference(
            referenceInterface = ReferenceProvider.class,
            cardinality = OPTIONAL_MULTIPLE,
            policy = DYNAMIC)
    private final List<ReferenceProvider> referenceProviders = new CopyOnWriteArrayList<ReferenceProvider>();

    protected void bindReferenceProviders(ReferenceProvider referenceProvider) {
        referenceProviders.add(referenceProvider);
    }

    protected void unbindReferenceProviders(ReferenceProvider referenceProvider) {
        referenceProviders.remove(referenceProvider);
    }

    @Override
    public void execute(WorkItem item, WorkflowSession session, MetaDataMap metaData)
            throws WorkflowException {
        try{
            Session userSession = session.getSession();
            ResourceResolver resolver = factory.getResourceResolver(userSession);

            Resource page = getResourceFromPayload(resolver, item, session.getSession());
            Resource jcrContent = resolver.getResource(page.getPath() + "/" + JcrConstants.JCR_CONTENT);

            Set<Reference> allReferences = new TreeSet<Reference>(new Comparator<Reference>() {
                public int compare(Reference o1, Reference o2) {
                    return o1.getResource().getPath().compareTo(o2.getResource().getPath());
                }
            });

            for (ReferenceProvider referenceProvider : referenceProviders) {
                allReferences.addAll(referenceProvider.findReferences(jcrContent));
            }

            Session participantSession = null;

            if (replicateAsParticipant(metaData)) {
                String approverId = resolveParticipantId(item, session);

                if (StringUtils.isNotEmpty(approverId)) {
                    participantSession = getParticipantSession(approverId, session);
                }
            }

            List<String> toPublish = getNotPublishedOrOutdatedReferences(page, allReferences, userSession);

            publishReferences(toPublish, (participantSession != null) ? participantSession : userSession);
        }catch(Exception e){
            throw new WorkflowException("Error publishing campaign", e);
        }
    }

    private Session getParticipantSession(String participantId, WorkflowSession session) {
        try {
            return session.getSession().impersonate(
                                new SimpleCredentials(participantId, new char[0]));
        } catch (Exception e) {
            log.warn(e.getMessage());
            return null;
        }
    }

    /**
     * See the session's history to find latest participant step or dynamic participant step and use it's current assignee
     * @param workItem
     * @param session
     * @return
     */
    private String resolveParticipantId(WorkItem workItem, WorkflowSession session) {
        List<HistoryItem> history = new ArrayList<HistoryItem>();

        try {
            history = session.getHistory(workItem.getWorkflow());

            for (int index = history.size() - 1; index >= 0; index--) {
                HistoryItem previous = history.get(index);
                String type = previous.getWorkItem().getNode().getType();

                if (type != null && (type.equals(WorkflowNode.TYPE_PARTICIPANT)
                        || type.equals(WorkflowNode.TYPE_DYNAMIC_PARTICIPANT))) {
                    return previous.getUserId();
                }
            }
        } catch (Exception e) {
            log.warn("Error getting participant id", e);
        }

        return null;
    }

    private boolean replicateAsParticipant(MetaDataMap args) {
        String processArgs = args.get(PROCESS_ARGS, String.class);

        if(StringUtils.isEmpty(processArgs)){
            return false;
        }

        String[] arguments = processArgs.split(",");

        for (String argument : arguments) {
            String[] split = argument.split("=");

            if (split.length == 2) {
                if (split[0].equalsIgnoreCase(REPLICATE_AS_PARTICIPANT)) {
                    return Boolean.parseBoolean(split[1]);
                }
            }
        }

        return false;
    }

    private List<String> getNotPublishedOrOutdatedReferences(Resource page, Set<Reference> allReferences,
                                            Session session){
        Resource resource = null;
        boolean canReplicate = false;

        List<String> toPublish = new ArrayList<String>();

        for (Reference reference : allReferences) {
            resource = reference.getResource();

            if (resource == null) {
                continue;
            }

            canReplicate = canReplicate(resource.getPath(), session);

            if(!canReplicate){
                log.warn("Skipping, No replicate permission on - " + resource.getPath());
                continue;
            }

            if(shouldReplicate(reference)){
                toPublish.add(resource.getPath());
            }

            if(reference.getType().equals(REF_CAMPAIGN)){
                collectCampaignReferences(resource.adaptTo(Page.class), toPublish, session, page);
            }
        }

        log.info("Publishing campaign assets - " + toPublish);

        return toPublish;
    }

    private boolean shouldReplicate(Reference reference){
        Resource resource = reference.getResource();
        ReplicationStatus replStatus = resource.adaptTo(ReplicationStatus.class);

        if (replStatus == null) {
            return true;
        }

        boolean doReplicate = false, published = false, outdated = false;
        long lastPublished = 0;

        published = replStatus.isDelivered() || replStatus.isActivated();

        if (published) {
            lastPublished = replStatus.getLastPublished().getTimeInMillis();
            outdated = lastPublished < reference.getLastModified();
        }

        if (!published || outdated) {
            doReplicate = true;
        }

        return doReplicate;
    }

    private void publishReferences(List<String> toPublish, Session session) throws Exception{
        ReplicationOptions opts = new ReplicationOptions();

        for(String path : toPublish){
            replicator.replicate(session, ReplicationActionType.ACTIVATE, path, opts);
        }
    }

    private void collectCampaignReferences(Page root, List<String> toPublish, Session session,
                                                Resource payloadPage) {
        ReferenceProvider assetReferenceProvider = null;

        for (ReferenceProvider referenceProvider : referenceProviders) {
            if(!(referenceProvider.getClass().getName().equals(ARP))){
                continue;
            }
            assetReferenceProvider = referenceProvider;
        }

        if(assetReferenceProvider == null){
            return;
        }

        Iterator<Page> experiences = root.listChildren();
        Page experience, offer; String assetPath;
        boolean canReplicate = false; String location;

        Resource contentRes = null;
        List<Reference> assetRefs = null;

        while(experiences.hasNext()) {
            experience = experiences.next();

            Iterator<Page> offers = experience.listChildren();

            while (offers.hasNext()) {
                offer = offers.next();

                contentRes = offer.getContentResource();

                location = contentRes.adaptTo(ValueMap.class).get(LOCATION, "");

                if(!location.startsWith(payloadPage.getPath() + "/jcr:content")){
                    log.debug("Publish campaign step skipping - " + offer.getPath());
                    continue;
                }

                toPublish.add(experience.getPath());

                toPublish.add(offer.getPath());

                ValueMap properties = offer.getProperties();
                String offerPath = properties.get(PN_OFFERPATH,"");

                if (StringUtils.isNotEmpty(offerPath)) {
                    toPublish.add(offerPath);
                }

                assetRefs = assetReferenceProvider.findReferences(contentRes);

                for(Reference ref : assetRefs){
                    assetPath = ref.getResource().getPath();
                    canReplicate = canReplicate(assetPath, session);

                    /*if(!canReplicate){
                        log.warn("Skipping, No replicate permission on - " + assetPath);
                        continue;
                    }

                    if(!shouldReplicate(ref)){
                        continue;
                    }*/

                    toPublish.add(assetPath);
                }
            }
        }
    }

    private static boolean canReplicate(String path, Session session) {
        try {
            AccessControlManager acMgr = session.getAccessControlManager();

            return acMgr.hasPrivileges(path, new Privilege[]{
                    acMgr.privilegeFromName(Replicator.REPLICATE_PRIVILEGE)
            });
        } catch (RepositoryException e) {
            return false;
        }
    }

    private Resource getResourceFromPayload(ResourceResolver resolver, WorkItem item,
                                            Session session) {
        if (!item.getWorkflowData().getPayloadType().equals("JCR_PATH")) {
            return null;
        }

        String path = item.getWorkflowData().getPayload().toString();

        return resolver.getResource(path);
    }
}


2) #240 collectCampaignReferences() method, adds the experiences of campaign to list of paths to be published collection

3) Process Step added in workflow Publish Campaign Example (copy of Publish Example)