Friday, 16 January 2015

JCR observation

JCR observation


JCR observation is a very cool concept, is it allows you to react on changes in the repository; this allows you to change any written data after the write in a centralized manner, instead of attaching a post processing step to each and every method before it is doing the save to the repository.

So JCR observation is a powerful concept, and is widely used for many different purposes.

But as it is a very fundamental concept and basis for a lot of features within CQ, you can do also a lot of harm in there. So I want to discuss the most important feature of JCR observation.

JCR observation is single-threaded

In Jackrabbit 1 and Jackrabbit 2 the JCR observation is a single-threaded mechanism. So for every event each registered listener is evaluated, and if there’s a match, the onEvent method of that listener is called by that thread.

Recommendation 1
The code executed in that onEvent method should be really fast. No long-standing calculation, no network access.

Recommendation 2
If you have a long-standing calculation or if you need to do network access, do it in a dedicated thread and run it asynchronously to the JCR observation.

Recommendation 3
Be careful when your event listener is writing to the repository as well. It there’s already a heavy write load, your event listener might block when it comes to writing to the repository. This will add additional delay to the JCR observation processing.

Recommendation 4
If your JCR observation handler needs to be active only on certain instances, make the register process configurable. Based on run mode or OSGI configuration do not do the “addEventListener” call. This avoids  the small overhead of executing an Event listener, which is not required on this instance.

Recommendation 5
If you get the WARN statement in your log files, that you have more than 200k pending events in the observation queue, your alarm clocks should ring. In that case all save() operations are delayed until the queue goes below that limit again (see http://www.docjar.com/html/api/org/apache/jackrabbit/core/observation/ObservationDispatcher.java.html); if you encounter this message, your first priority should be to analyze your observation listeners and speed them up.

Recommendation 6
If you want to know which observation listener consumes how much time, you should set the class “org.apache.jackrabbit.core.observation.EventConsumer” to DEBUG; it will then print for each event (!), which listener consumed how much time.


A good example for this are the CQ DAM Asset Update workflows. There are many ways how you can ingest assets into your CQ: direct upload via browser, webdav, sync from Creative Cloud, uploads via your own bulk ingestions methods, the Sling POST servlet, … And instead of attaching the metadata extraction, the rendition generation and all the other stuff to each of these methods, a single centralized process takes care of it. It doesn’t matter that this process is asynchronous to the ingestion itself. In this case it’s a workflow, but the workflow triggers are also directly based on JCR observation.

Conclusion, if you use use JCR observation, be aware, that you the code fast you want to execute. Everything which exceeds a few milliseconds should be offloaded to a separate thread. And check, that you run only the observation listeners which are really required to run.

Thursday, 8 January 2015

Multi image component using MultiField - AEM 5.6 / 6.0

Create Component


1) In your CRXDE Lite http://localhost:4502/crx/de, create below folder and save changes

                      /apps/imagemultifield

2) Copy the component /libs/foundation/components/logo and paste it in path /apps/imagemultifield

3) Rename /apps/imagemultifield/logo to /apps/imagemultifield/imagemultifield

4) Rename /apps/imagemultifield/imagemultifield/logo.jsp to /apps/imagemultifield/imagemultifield/imagemultifield.jsp

5) Change the following properties of /apps/imagemultifield/imagemultifield

                     componentGroup - My Components
                     jcr:title - Image MultiField Component

6) Add the following to dialog (/apps/imagemultifield/imagemultifield/dialog) xml



<?xml version="1.0" encoding="UTF-8"?>
    jcr:primaryType="cq:Dialog"
    activeTab="{Long}0"
    title="Multi Image"
    xtype="tabpanel">
    <items jcr:primaryType="cq:WidgetCollection">
        <basic
            jcr:primaryType="cq:Widget"
            title="Images"
            xtype="panel">
            <items jcr:primaryType="cq:WidgetCollection">
                <images
                    jcr:primaryType="cq:Widget"
                    border="false"
                    hideLabel="true"
                    name="./images"
                    xtype="imagemultifield">
                    <fieldConfig
                        jcr:primaryType="cq:Widget"
                        border="false"
                        hideLabel="true"
                        layout="form"
                        padding="10px 0 0 100px"
                        xtype="imagemultifieldpanel">
                        <items jcr:primaryType="cq:WidgetCollection">
                            <image
                                jcr:primaryType="cq:Widget"
                                cropParameter="./imageCrop"
                                ddGroups="[media]"
                                fileNameParameter="./imageName"
                                fileReferenceParameter="./imageReference"
                                height="250"
                                mapParameter="./imageMap"
                                name="./image"
                                rotateParameter="./imageRotate"
                                sizeLimit="100"
                                xtype="imagemultifieldsmartimage"/>
                        </items>
                    </fieldConfig>
                </images>
            </items>
        </basic>
    </items>
</jcr:root>



Add JS Logic and Register XTypes


1) Create node /apps/imagemultifield/imagemultifield/clientlib of type cq:ClientLibraryFolder and add the following properties

             categories - String - cq.widgets

2) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/js.txt and add the following

              imagemultifield.js

3) Create file (type nt:file) /apps/imagemultifield/imagemultifield/clientlib/imagemultifield.js and add the following code




CQ.Ext.ns("ImageMultiField");
 
ImageMultiField.Panel = CQ.Ext.extend(CQ.Ext.Panel, {
    initComponent: function () {
        ImageMultiField.Panel.superclass.initComponent.call(this);
 
        var multifield = this.findParentByType('imagemultifield');
        var image = this.find('xtype', 'imagemultifieldsmartimage')[0];
 
        var imageName = multifield.nextImageName;
 
        if(!imageName){
            imageName = image.name;
 
            if(!imageName){
                imageName = "demo";
            }else if(imageName.indexOf("./") == 0){
                imageName = imageName.substr(2); //get rid of ./
            }
 
            var suffix = multifield.nextImageNum = multifield.nextImageNum + 1;
            imageName = this.name + "/" + imageName + "-" + suffix;
        }
 
        image.name = imageName;
 
        var changeParams = ["cropParameter", "fileNameParameter","fileReferenceParameter",
                                "mapParameter","rotateParameter" ];
 
        CQ.Ext.each(changeParams, function(cItem){
            if(image[cItem]){
                image[cItem] = imageName + "/" +
                    ( image[cItem].indexOf("./") == 0 ? image[cItem].substr(2) : image[cItem]);
            }
        });
 
        CQ.Ext.each(image.imageToolDefs, function(toolDef){
            toolDef.transferFieldName = imageName + toolDef.transferFieldName.substr(1);
            toolDef.transferField.name = toolDef.transferFieldName;
        });
    },
 
    setValue: function (record) {
        var multifield = this.findParentByType('imagemultifield');
        var image = this.find('xtype', 'imagemultifieldsmartimage')[0];
 
        var recCopy = CQ.Util.copyObject(record);
 
        var imagePath = multifield.path + "/" + image.name;
        var imgRec = recCopy.get(image.name);
 
        for(var x in imgRec){
            if(imgRec.hasOwnProperty(x)){
                recCopy.data[x] = imgRec[x];
            }
        }
 
        recCopy.data[this.name.substr(2)] = undefined;
 
        var fileRefParam = image.fileReferenceParameter;
        image.fileReferenceParameter = fileRefParam.substr(fileRefParam.lastIndexOf("/") + 1);
 
        image.processRecord(recCopy, imagePath);
        image.fileReferenceParameter = fileRefParam;
    },
 
    validate: function(){
        return true;
    }
});
 
CQ.Ext.reg("imagemultifieldpanel", ImageMultiField.Panel);
 
ImageMultiField.SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
    syncFormElements: function() {
        if(!this.fileNameField.getEl().dom){
            return;
        }
 
        ImageMultiField.SmartImage.superclass.syncFormElements.call(this);
    } ,
 
    afterRender: function() {
        ImageMultiField.SmartImage.superclass.afterRender.call(this);
 
        var dialog = this.findParentByType('dialog');
        var target = this.dropTargets[0];
 
        if (dialog && dialog.el && target.highlight) {
            var dialogZIndex = parseInt(dialog.el.getStyle("z-index"), 10);
 
            if (!isNaN(dialogZIndex)) {
                target.highlight.zIndex = dialogZIndex + 1;
            }
        }
 
        var multifield = this.findParentByType('multifield');
        multifield.dropTargets.push(target);
 
        this.dropTargets = undefined;
    }
});
 
CQ.Ext.reg('imagemultifieldsmartimage', ImageMultiField.SmartImage);
 
CQ.Ext.override(CQ.form.SmartImage.ImagePanel, {
    addCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);
 
        if(imageCanvas){
            imageCanvas.addClass(clazz);
        }
    },
 
    removeCanvasClass: function(clazz) {
        var imageCanvas = CQ.Ext.get(this.imageCanvas);
 
        if(imageCanvas){
            imageCanvas.removeClass(clazz);
        }
    }
});
 
CQ.Ext.override(CQ.form.SmartImage.Tool, {
    processRecord: function(record) {
        var iniValue = record.get(this.transferFieldName);
 
        if(!iniValue && ( this.transferFieldName.indexOf("/") !== -1 )){
            iniValue = record.get(this.transferFieldName.substr(this.transferFieldName.lastIndexOf("/") + 1));
        }
 
        if (iniValue == null) {
            iniValue = "";
        }
 
        this.initialValue = iniValue;
    }
});
 
CQ.Ext.override(CQ.form.MultiField.Item, {
    reorder: function(item) {
        if(item.field && item.field.xtype == "imagemultifieldpanel"){
            var c = this.ownerCt;
            var iIndex = c.items.indexOf(item);
            var tIndex = c.items.indexOf(this);
 
            if(iIndex < tIndex){ //user clicked up
                c.insert(c.items.indexOf(item), this);
                this.getEl().insertBefore(item.getEl());
            }else{//user clicked down
                c.insert(c.items.indexOf(this), item);
                this.getEl().insertAfter(item.getEl());
            }
 
            c.doLayout();
        }else{
            var value = item.field.getValue();
            item.field.setValue(this.field.getValue());
            this.field.setValue(value);
        }
    }
});
 
ImageMultiField.MultiField = CQ.Ext.extend(CQ.form.MultiField , {
    Record: CQ.data.SlingRecord.create([]),
    nextImageNum: 0,
    nextImageName: undefined,
 
    initComponent: function() {
        ImageMultiField.MultiField.superclass.initComponent.call(this);
 
        var imagesOrder = new CQ.Ext.form.Hidden({
            name: this.getName() + "/order"
        });
 
        this.add(imagesOrder);
 
        var dialog = this.findParentByType('dialog');
 
        dialog.on('beforesubmit', function(){
            var imagesInOrder = this.find('xtype','imagemultifieldsmartimage');
            var order = [];
 
            CQ.Ext.each(imagesInOrder , function(image){
                order.push(image.name.substr(image.name.lastIndexOf("/") + 1))
            });
 
            imagesOrder.setValue(JSON.stringify(order));
        },this);
 
        this.dropTargets = [];
    },
 
    addItem: function(value){
        if(!value){
            value = new this.Record({},{});
        }
        ImageMultiField.MultiField.superclass.addItem.call(this, value);
    },
 
    processRecord: function(record, path) {
        if (this.fireEvent('beforeloadcontent', this, record, path) !== false) {
            this.items.each(function(item) {
                if(item.field && item.field.xtype == "imagemultifieldpanel"){
                    this.remove(item, true);
                }
            }, this);
 
            var images = record.get(this.getName());
            this.nextImageNum = 0;
 
            if (images) {
                var oName = this.getName() + "/order";
                var oValue = record.get(oName) ? record.get(oName) : "";
 
                var iNames = JSON.parse(oValue);
                var highNum, val;
 
                CQ.Ext.each(iNames, function(iName){
                    val = parseInt(iName.substr(iName.indexOf("-") + 1));
 
                    if(!highNum || highNum < val){
                        highNum = val;
                    }
 
                    this.nextImageName = this.getName() + "/" + iName;
                    this.addItem(record);
                }, this);
 
                this.nextImageNum = highNum;
            }
 
            this.nextImageName = undefined;
 
            this.fireEvent('loadcontent', this, record, path);
        }
    }
});
 
CQ.Ext.reg('imagemultifield', ImageMultiField.MultiField);

Rendering Images


1) Images created in the CRX using MultiImage componet are rendered using /apps/imagemultifield/imagemultifield/imagemultifield.jsp. Add the following code in jsp
<%@include file="/libs/foundation/global.jsp"%>
 
<%@ page import="java.util.Iterator" %>
<%@ page import="com.day.cq.wcm.foundation.Image" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
 
<%
    Iterator<Resource> children = resource.listChildren();
 
    if(!children.hasNext()){
%>
 
        Click here to add images
 
<%
    }else{
        Resource imagesResource = children.next();
        ValueMap map = imagesResource.adaptTo(ValueMap.class);
        String order = map.get("order", String.class);
 
        Image img = null; String src = null;
        JSONArray array = new JSONArray(order);
 
        for(int i = 0; i < array.length(); i++){
            img = new Image(resource);
            img.setItemName(Image.PN_REFERENCE, "imageReference");
            img.setSuffix(String.valueOf(array.get(i)));
            img.setSelector("img");
 
            src = img.getSrc();
%>
            <img src='<%=src%>'/>
<%
        }
    }
%>