AEM CQ 56 - Panel in CQ.form.MultiField

Goal


Create a Composite Multifield Component (CQ.form.MultiField) with panel field config to store data as json, in Classic UI. For Touch UI multifield check this post

Logic for AEM 60 and AEM 56 is same, i just didn't want to remove the link to 56 code

To store multi field values as child nodes (not json) check this post


AEM 60

Demo | Package Install


Bug Fixes

Reordering items overrides empty fields, Using CQ.form.Selection.PATH_PLACEHOLDER like $PATH/countries.options.json doesn't load options of a selection drop down in multi-field-panel - Demo | Package Install

LoadContent event does not fire on multifield items; Drag and drop to pathfield (in multifield) doesn't work; Widgets added in multifield in a tab other than the visible one, shrink; supporting allowBlank on widgets added in multifield - Demo | Package Install






AEM 56

Source code (not package install) and Demo


Prerequisites


If you are new to CQ

1) Read this post on how to create a sample page component

2) Here is another blog post on multifield fieldConfig customization

Create Component


1) Create component /apps/multifieldpanel/multifieldpanel with the following properties




2) Create clientlib /apps/multifieldpanel/multifieldpanel/clientlib of type cq:ClientLibraryFolder with the following properties




3) Create file /apps/multifieldpanel/multifieldpanel/clientlib/multipanel.js and add the following code. Here we are creating a panel extension and registering it as xtype mymultipanel

var MyClientLib = MyClientLib || {};

MyClientLib.MyMultiPanel = CQ.Ext.extend(CQ.Ext.Panel, {
    panelValue: '',

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

    initComponent: function () {
        MyClientLib.MyMultiPanel.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);
    },

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

            if(!pData[i.dName]){
                return;
            }

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

    validate: function(){
        return true;
    },

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

CQ.Ext.reg("mymultipanel", MyClientLib.MyMultiPanel);

4) Create file /apps/multifieldpanel/multifieldpanel/clientlib/js.txt and add the following code

               multipanel.js

5) Create dialog /apps/multifieldpanel/multifieldpanel/dialog for component above with the following properties. Here,

       a) The node /apps/multifieldpanel/multifieldpanel/dialog/items/items/tab1/items/map/fieldConfig has xtype mymultipanel

       b) Each widget in the multipanel (eg. /apps/multifieldpanel/multifieldpanel/dialog/items/items/tab1/items/map/fieldConfig/items/product-year-value) should have a dName property, which is read by multi panel to store value entered by author for the field

<?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="mymultipanel">
                            <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-lowStock-value
                                    jcr:primaryType="cq:Widget"
                                    dName="lowStock"
                                    fieldLabel="Low Stock ?"
                                    width="25"
                                    xtype="checkbox"/>
                            </items>
                        </fieldConfig>
                    </map>
                </items>
            </tab1>
        </items>
    </items>
</jcr:root>



6) Add the following code in /apps/multifieldpanel/multifieldpanel/multifieldpanel.jsp

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

<div style="display: block; border-style: solid; border-width: 1px; margin: 10px; padding: 10px">
    <b>Multi Field Sample</b>

    <%
        try {
            Property property = null;

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

            if (property != null) {
                JSONObject obj = null;
                Value[] values = null;

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

                for (Value val : values) {
                    obj = new JSONObject(val.getString());
    %>
                        Year : <b><%= obj.get("year") %></b>,
                        Price : <b><%= obj.get("price") %></b>,
                        Version : <b><%= obj.get("version")%></b>,
                        Low Stock : <b><%=obj.get("lowStock")%></b>
    <%
                }
            } else {
    %>
                Add values in dialog
    <%
            }
        } catch (Exception e) {
            e.printStackTrace(new PrintWriter(out));
        }
    %>

</div>



AEM CQ - Retrieve Groups and Users from CRX using RMI

Goal


Create a sample java RMI client to connect to CQ (AEM 56) and access the groups and users in CRX repository. For using JCR Remoting or DavEx check this post

Update: There are issue connecting to AEM 6 (Oak) using RMI

Download


Download the necessary jars (JCR, SL4J etc.) from Adobe repo - http://repo.adobe.com/nexus/content/groups/public/ and add them to classpath. You'll need the following jars

.m2\repository\javax\jcr\jcr\2.0\jcr-2.0.jar
.m2\repository\org\apache\jackrabbit\jackrabbit-jcr-rmi\2.6.2\jackrabbit-jcr-rmi-2.6.2.jar
.m2\repository\org\slf4j\slf4j-api\1.5.8\slf4j-api-1.5.8.jar
.m2\repository\org\slf4j\slf4j-simple\1.5.2\slf4j-simple-1.5.2.jar

Setup


1) Access the CQ configuration console http://localhost:4502/system/console/configMgr in a browser and find the bundle Apache Sling JCR Repository RMI Registrar

2) Click on Apache Sling JCR Repository RMI Registrar and change the port number from 1099 to 1199 to start RMI bundle

3) Run the following client

package apps;

import org.apache.jackrabbit.rmi.client.ClientNode;
import org.apache.jackrabbit.rmi.client.ClientRepositoryFactory;

import javax.jcr.NodeIterator;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;

public class AemGroupsUsers {
    public static void main(String[] args) throws Exception {
        String crxApplicationName = "virtual-crx";
        String repoUrl = "//localhost:1199/" + crxApplicationName;
        String workspace = "crx.default";
        String username = "admin";

        char[] password = "admin".toCharArray();

        ClientRepositoryFactory factory = new ClientRepositoryFactory();
        Repository repository = factory.getRepository(repoUrl);

        Session session = repository.login(new SimpleCredentials(username, password), workspace);
        QueryManager qm = session.getWorkspace().getQueryManager();

        String stmt = "//element(*,rep:Group) order by @rep:principalName";
        Query q = qm.createQuery(stmt, Query.XPATH);

        NodeIterator results = q.execute().getNodes();
        ClientNode node = null;

        System.out.println("Groups Query : " + stmt);

        while (results.hasNext()) {
            node = (ClientNode) results.next();
            System.out.print(node.getName() + ",");
        }

        stmt = "//element(*,rep:User) order by @rep:principalName";
        q = qm.createQuery(stmt, Query.XPATH);

        results = q.execute().getNodes();

        System.out.println("\n\nUsers Query : " + stmt);

        while (results.hasNext()) {
            node = (ClientNode) results.next();
            System.out.print(node.getName() + ",");
        }

        session.logout();
    }
}

Favorites (Bookmarks) Widget Component

Goal


This blog post is on creating a Favorites (Bookmarks) Component. For dashboard use-cases where an author has to group a set of page links and put it on a page for quick access, the following approach could be useful. It also explains

             1) Creating custom widget in CQ

             2) Using CQ.form.MultiField

             3) Extending CQ.form.CompositeField

             4) Sample Ajax Sling Servlet

             5) Customizing CQ.form.PathField search window with new buttons and tree loader




In the demo video

             1) User admin logs in and shows the permissions for user John. John has read permissions on all /content nodes but modify on sites first-app-demo-site and geometrixx only

             2)  John logs in, creates the favorites component, adds some links, rearranges them, deletes etc.

Source code and Demo

Thanks to this blogger for giving a start http://cq.shishank.info/2011/12/19/multifield-with-custom-xtype/

Prerequisites


If you are new to CQ

1) Read this post on how to create a sample page component

2) Read this post on how to setup your IDE and create an OSGI component

Create Servlet


1) The first step is creating and deploying a servlet to read content page nodes from CRX. Here the servlet PageNodesServlet ( deployed as OSGI component ) accepts content path and returns the nodes in a json response. The pathfield widget treeloader requests this servlet for nodes and based on the input parameters ( path and type) returns all child nodes of a path or just the nodes for which user has modify permission

package apps.mycomponents.favorites;

import com.day.cq.commons.LabeledResource;
import com.day.text.Text;
import org.apache.commons.collections.Predicate;
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.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
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.jcr.Session;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

@SlingServlet (
    paths="/bin/mycomponents/favorites/pagenodes",
    methods = "GET",
    metatype = true,
    label = "Page Nodes Servlet"
)
public class PageNodesServlet extends SlingAllMethodsServlet {
    private static final long serialVersionUID = 1L;

    private static final Logger LOG = LoggerFactory.getLogger(PageNodesServlet.class);

    class FolderOrPagePredicate implements Predicate {
        @Override
        public boolean evaluate(Object o) {
            Resource resource = (Resource)o;
            return resource.getResourceType().equals("sling:Folder") || resource.getResourceType().equals("cq:Page");
        }
    }

    private final Predicate FOLDER_PAGE_PREDICATE = new FolderOrPagePredicate();

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

        String path = request.getParameter("path");
        String type = request.getParameter("type");

        if( ( type == null ) || type.trim().equals("")){
            type = "all";
        }

        ResourceResolver resolver = request.getResourceResolver();
        Resource res = resolver.getResource(path);
        Session userSession = resolver.adaptTo(Session.class);

        List<Resource> children = new LinkedList<Resource>();
        JSONWriter jw = new JSONWriter(response.getWriter());

        try{
            for (Iterator iter = resolver.listChildren(res); iter.hasNext(); ) {
                Resource child = (Resource)iter.next();

                if(FOLDER_PAGE_PREDICATE.evaluate(child) && hasPermission(type, userSession, child.getPath())){
                    children.add(child);
                }
            }

            write(request, jw, children, type);
        }catch(Exception e){
            LOG.error("Error getting nodes",e);
            throw new ServletException(e);
        }
    }

    private boolean hasPermission(String type, Session userSession, String resourcePath) throws Exception{
        return "all".equals(type) || userSession.hasPermission(resourcePath, "set_property");
    }

    private boolean hasChildren(Resource res, ResourceResolver resolver, String type ) throws Exception{
        Session userSession = resolver.adaptTo(Session.class);
        Iterator<Resource> iter = resolver.listChildren(res);

        while (iter.hasNext()) {
            Resource child = iter.next();
            if (FOLDER_PAGE_PREDICATE.evaluate(child) && hasPermission(type, userSession, child.getPath())) {
                return true;
            }
        }

        return false;
    }

    public void write(SlingHttpServletRequest request, JSONWriter out, List<Resource> list, String type) throws Exception{
        ResourceResolver resolver = request.getResourceResolver();
        out.array();

        for (Resource resource : list) {
            out.object();

            LabeledResource lr = resource.adaptTo(LabeledResource.class);
            String name = Text.getName(resource.getPath());

            out.key("name").value(name);
            out.key("type").value(resource.getResourceType());

            boolean hasChildren = hasChildren(resource, resolver, type);

            out.key("cls").value(hasChildren ? "folder" : "file");

            if (!hasChildren) {
                out.key("leaf").value(true);
            }

            String text;

            if (lr == null) {
                text = name;
            } else {
                text = lr.getTitle() == null ? name : lr.getTitle();
            }

            out.key("text").value(text);
            out.endObject();
        }

        out.endArray();
    }
}

2) Install ( this post explains how-to ) the servlet; you should see folder /apps/favorites/install created in CRXDE Lite (http://localhost:4502/crx/de) with bundle jar

3) Access the servlet in browser with url http://localhost:4502/bin/mycomponents/favorites/pagenodes?path=/content and nodes under /content are returned in a json response

Create Component


1) Create the component (of type cq:Component) /apps/favorites/favorites



2) Create dialog /apps/favorites/favorites/dialog (of type cq:Dialog)


3) Create node /apps/favorites/favorites/clientlib ( of type cq:ClientLibraryFolder ) with following properties





4) Create file node /apps/favorites/favorites/clientlib/favorites.js and add the following code. Here we are creating a new ExtJS xtype for favorites field widget and registering it for use in multifield widget of dialog, which we are going to configure in the next steps..

var MyClientLib = {
    dataUrl: ''
};

MyClientLib.FavoritesField = CQ.Ext.extend(CQ.form.CompositeField, {
    favText: null,
    favPath: null,
    fav: null,

    constructor: function(config){
        config = config || {};
        var defaults = { "labelWidth" : 150, "layout" : "form", border: true,
            padding: "10px", width: 500, boxMaxWidth : 520 };
        config = CQ.Util.applyDefaults(config, defaults);
        MyClientLib.FavoritesField.superclass.constructor.call(this, config);
    },

    initComponent: function () {
        MyClientLib.FavoritesField.superclass.initComponent.call(this);

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

        this.add(this.fav);

        this.add(new CQ.Ext.form.Label({
            text: "Display Text"
        }));

        this.favText = new CQ.Ext.form.TextField({
            width: 300,
            allowBlank: true
        });

        this.add(this.favText);

        this.add(new CQ.Ext.form.Label({
            text: "Select Path"
        }));

        var handlerFn = function(thisObj, type){
            var treePanel = thisObj.treePanel;
            var path = thisObj.path;
            thisObj.treeLoader.dataUrl = MyClientLib.dataUrl + "?type=" + type;
            thisObj.treeLoader.load(thisObj.treePanel.root, function(){
                treePanel.selectPath(path);
            });
        }

        var buttons = [ new CQ.Ext.Button( { text: "All", width: 68, tooltip: 'Show all tree nodes', handler: function(){ handlerFn(this,'all'); }} ),
            new CQ.Ext.Button( { text: "Modify", width: 95, tooltip: 'Show nodes with modify permission only', handler: function(){ handlerFn(this,'write'); } } ),
            CQ.Dialog.OK, CQ.Dialog.CANCEL
        ];

        this.favPath = new CQ.form.PathField({
            treeLoader: new CQ.Ext.tree.TreeLoader({
                dataUrl:MyClientLib.dataUrl,
                requestMethod: "GET"
            }),
            browseDialogCfg: { "buttons" : buttons},
            allowBlank: false,
            width: 300,
            listeners: {
                dialogclose: {
                    fn: function(f){
                        var selNode = this.browseDialog.treePanel.getSelectionModel().getSelectedNode();
                        this.ownerCt.favText.setValue(selNode.text);
                    }
                }
            }
        });

        this.add(this.favPath);

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

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

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

    getValue: function () {
        if(this.favPath.el && this.favPath.el.dom){
            var link = {
                "url" : this.favPath.getValue(),
                "text" : this.favText.getValue()
            };

            return JSON.stringify(link);
        }

        return null;
    },

    setValue: function (value) {
        var link = JSON.parse(value);
        this.favText.setValue(link.text);
        this.favPath.setValue(link.url);
        this.fav.setValue(value);
    }
});

CQ.Ext.reg("myfavoritesfield", MyClientLib.FavoritesField);

5) Create file node /apps/favorites/favorites/clientlib/js.txt and add

                  favorites.js

6) Set the title property of /apps/favorites/favorites/dialog/items/items/tab1 to Add

7) Create node /apps/favorites/favorites/dialog/items/items/tab1/items of type cq:WidgetCollection

8) Create node /apps/favorites/favorites/dialog/items/items/tab1/items/favfield of type cq:Widget



9) Create node /apps/favorites/favorites/dialog/items/items/tab1/items/favfield/fieldConfig of type cq:Widget with xtype myfavoritesfield

10) Add the following code in /apps/favorites/favorites/favorites.jsp

<%@ page import="com.day.cq.wcm.api.WCMMode" %>
<%@ page import="org.osgi.framework.FrameworkUtil" %>
<%@ page import="apps.mycomponents.favorites.PageNodesServlet" %>
<%@ page import="org.osgi.framework.Bundle" %>
<%@ page import="org.osgi.framework.ServiceReference" %>
<%@ page import="org.apache.sling.commons.json.JSONObject" %>
<%@ page import="java.io.PrintWriter" %>
<%@include file="/libs/foundation/global.jsp" %>
<%@page session="false" %>

<cq:includeClientLib categories="mycomponents.favorites"/>

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

    <%
        try {
            Property property = null;

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

            if (property != null) {
                JSONObject obj = null;
                String resourcePath = null;
                Value[] favorites = null;

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

                for (Value val : favorites) {
                    obj = new JSONObject(val.getString());
                    resourcePath = xssAPI.getValidHref(String.valueOf(obj.get("url")) + ".html");
    %>
                    <a href="<%=resourcePath%>"><%=obj.get("text")%></a>
                    <br><br>
    <%
                }
            } else {
    %>
                Configure the favorite pages in dialog
                <br><br>
    <%
            }
    %>

</div>

<%
        if (WCMMode.fromRequest(request) != WCMMode.DISABLED) {
            Bundle bundle = FrameworkUtil.getBundle(PageNodesServlet.class);
            ServiceReference[] services = bundle.getRegisteredServices();

            //assuming we have only one servlet as osgi service
            String sPath = String.valueOf(services[0].getProperty("sling.servlet.paths"));

%>
            <script type="text/javascript">
                CQ.Ext.onReady(function () {
                    MyClientLib.dataUrl = '<%=sPath%>';
                })
            </script>
<%
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>


11) Save All and Done; add the component on a page and you should be able to bookmark site pages



HTML 5 Smart Image Component Custom Aspect Ratios

Goal


Create a html 5 smart image component that supports custom aspect ratios, image cropping. An author can create images of different aspect ratios using the same image component. Before you proceed, the process explained here is not Adobe suggested approach, it's just a thought; Package install, Source code and Video demonstration are available for download..





Prerequisites


If you are new to CQ pls visit this blog post; it explains the page component basics and setting up your IDE

Create the Component


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

                      /apps/imagecustomar

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

3) Rename /apps/imagecustomar/logo to /apps/imagecustomar/image

4) Rename /apps/imagecustomar/image/logo.jsp to /apps/imagecustomar/image/image.jsp

5) Change the package statement of /apps/imagecustomar/image/img.GET.java from libs.foundation.components.logo to apps.imagecustomar.image

6) Change the following properties of /apps/imagecustomar/image

                     componentGroup - My Components
                     jcr:title - Image with Custom Aspect Ratios

7) Rename /apps/imagecustomar/image/design_dialog to /apps/imagecustomar/image/dialog

8) Delete /apps/imagecustomar/image/dialog/items/basic

9) Replace the code in /apps/imagecustomar/image/image.jsp with the following...

<%@include file="/libs/foundation/global.jsp" %>
<%@ page import="com.day.cq.commons.Doctype,
                 com.day.cq.wcm.foundation.Image,
                 java.io.PrintWriter" %>
<%
    try {
        Resource res = null;

        if (currentNode.hasProperty("imageReference")) {
            res = resource;
        }
%>

<%
        if (res == null) {
%>
            Configure Image
<%
        } else {
            Image img = new Image(res);
            img.setItemName(Image.NN_FILE, "image");
            img.setItemName(Image.PN_REFERENCE, "imageReference");
            img.setSelector("img");
            img.setDoctype(Doctype.fromRequest(request));
            img.setAlt("Home");
            img.draw(out);
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>


10) Add this Image component on a page and select an image. The image dialog with cropping features enabled looks like below. Here, Free crop has default aspect ratio (0,0)




10) We now have a basic image component with ootb features ( crop, rotate etc ). Let us modify this component to add custom aspect ratios; in the process we also create and register a new ExtJS xtype

Create and Register xtype


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

             categories - String[] - mycomponent.imagear
             dependencies - String[] - cq.widgets

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

              imagear.js

3) Create file /apps/imagecustomar/image/clientlib/imagear.js. For now, add the following code

              alert("hi");

4) Modify /apps/imagecustomar/image/image.jsp to include the clientlib created above

               <cq:includeClientLib categories="mycomponent.imagear"/>

5) Save changes and access the component dialog; an alert "hi" should popup, confirming the js file load.

6) Add the following JS code to imagear.js. This logic adds the custom aspect ratios UI changes to crop tool

var MyClientLib = MyClientLib || {};

MyClientLib.Html5SmartImage = CQ.Ext.extend(CQ.html5.form.SmartImage, {
    crops: {},

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

        var aRatios = {
            "freeCrop": {
                "value": "0,0",
                "text": CQ.I18n.getMessage("Free crop")
            }
        };

        var tObj = this;

        $.each(config, function (key, value) {
            if (key.endsWith("AspectRatio")) {
                var text = config[key + "Text"];

                if (!text) {
                    text = key;
                }

                if (!value) {
                    value = "0,0";
                }

                aRatios[key] = {
                    "value": value,
                    "text": text
                };

                tObj.crops[key] = { text: text, cords : ''};
            }
        });

        var defaults = { "cropConfig": { "aspectRatios": aRatios } };
        config = CQ.Util.applyDefaults(config, defaults);

        MyClientLib.Html5SmartImage.superclass.constructor.call(this, config);
    },

    initComponent: function () {
        MyClientLib.Html5SmartImage.superclass.initComponent.call(this);

        var imgTools = this.imageToolDefs;
        var cropTool;

        if(imgTools){
            for(var x = 0; x < imgTools.length; x++){
                if(imgTools[x].toolId == 'smartimageCrop'){
                    cropTool = imgTools[x];
                    break;
                }
            }
        }

        if(!cropTool){
            return;
        }

        for(var x in this.crops){
            if(this.crops.hasOwnProperty(x)){
                var field = new CQ.Ext.form.Hidden({
                    id: x,
                    name: "./" + x
                });

                this.add(field);

                field = new CQ.Ext.form.Hidden({
                    name: "./" + x + "Text",
                    value: this.crops[x].text
                });

                this.add(field);
            }
        }

        var userInterface = cropTool.userInterface;

        this.on("loadimage", function(){
            var aRatios = userInterface.aspectRatioMenu.findByType("menucheckitem");

            if(!aRatios){
                return;
            }

            for(var x = 0; x < aRatios.length; x++){
                if(aRatios[x].text !== "Free crop"){
                    aRatios[x].on('click', function(radio){
                        var key = this.getCropKey(radio.text);

                        if(!key){
                            return;
                        }

                        if(this.crops[key].cords){
                            this.setCoords(cropTool, this.crops[key].cords);
                        }else{
                            var field = CQ.Ext.getCmp(key);
                            this.crops[key].cords = this.getRect(radio, userInterface);
                            field.setValue(this.crops[key].cords);
                        }
                    },this);
                }

                var key = this.getCropKey(aRatios[x].text);

                if(key && this.dataRecord && this.dataRecord.data[key]){
                    this.crops[key].cords = this.dataRecord.data[key];

                    var field = CQ.Ext.getCmp(key);
                    field.setValue(this.crops[key].cords);
                }
            }
        });

        cropTool.workingArea.on("contentchange", function(changeDef){
            var aRatios = userInterface.aspectRatioMenu.findByType("menucheckitem");
            var aRatioChecked;

            if(aRatios){
                for(var x = 0; x < aRatios.length; x++){
                    if(aRatios[x].checked === true){
                        aRatioChecked = aRatios[x];
                        break;
                    }
                }
            }

            if(!aRatioChecked){
                return;
            }

            var key = this.getCropKey(aRatioChecked.text);
            var field = CQ.Ext.getCmp(key);

            this.crops[key].cords = this.getRect(aRatioChecked, userInterface);
            field.setValue(this.crops[key].cords);
        }, this);
    },

    getCropKey: function(text){
        for(var x in this.crops){
            if(this.crops.hasOwnProperty(x)){
                if(this.crops[x].text == text){
                    return x;
                }
            }
        }

        return null;
    },

    getRect: function (radio, ui) {
        var ratioStr = "";
        var aspectRatio = radio.value;

        if ((aspectRatio != null) && (aspectRatio != "0,0")) {
            ratioStr = "/" + aspectRatio;
        }

        if (ui.cropRect == null) {
            return ratioStr;
        }

        return ui.cropRect.x + "," + ui.cropRect.y + "," + (ui.cropRect.x + ui.cropRect.width) + ","
            + (ui.cropRect.y + ui.cropRect.height) + ratioStr;
    },

    setCoords: function (cropTool, cords) {
        cropTool.initialValue = cords;
        cropTool.onActivation();
    }
});

CQ.Ext.reg("myhtml5smartimage", MyClientLib.Html5SmartImage);


7) To create the following image source paths...

/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/2To1AspectRatio.jpg
/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/9To1AspectRatio.jpg
/content/firstapp-demo-site/test/_jcr_content/par/image.img.png/5To1AspectRatio.jpg

Replace the code in /apps/imagecustomar/image/image.jsp with below code. This jsp renders the cropped custom aspect ratio images...

<%@include file="/libs/foundation/global.jsp" %>
<%@ page import="com.day.cq.wcm.foundation.Image,
                 java.io.PrintWriter" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>

<cq:includeClientLib js="mycomponent.imagear"/>

<%
    try {
        Resource res = null;

        if (currentNode.hasProperty("imageReference")) {
            res = resource;
        }

        if (res == null) {
%>
            Configure Image
<%
        } else {
            PropertyIterator itr = currentNode.getProperties();
            Property prop = null; String text = "";
            Map<String, String> aMap = new HashMap<String, String>();

            while(itr.hasNext()){
                prop = itr.nextProperty();

                if(prop.getName().endsWith("AspectRatio")){
                    text = prop.getName();

                    if(currentNode.hasProperty(prop.getName() + "Text")){
                        text = currentNode.getProperty(prop.getName() + "Text").getString();
                    }

                    aMap.put(prop.getName(), text);
                }
            }

            Image img = null; String src = null;

            if(aMap.isEmpty()){
%>
                Cropped Images with custom aspect ratios not available
<%
            }else{
                for(Map.Entry entry : aMap.entrySet()){
                    img = new Image(res);
                    img.setItemName(Image.PN_REFERENCE, "imageReference");
                    img.setSuffix(entry.getKey() + ".jpg");
                    img.setSelector("img");

                    src = img.getSrc();
%>
                    <br><br><b><%=entry.getValue()%></b><br><br>
                    <img src='<%=src%>'/>
<%
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace(new PrintWriter(out));
    }
%>

8) Add the following code to /apps/imagecustomar/image/img.GET.java. This java logic reads crop co-ordinates from CRX and outputs the image bytes to browser

package apps.imagecustomar.image;

import java.awt.*;
import java.io.IOException;
import java.io.InputStream;

import javax.jcr.RepositoryException;
import javax.jcr.Property;
import javax.servlet.http.HttpServletResponse;

import com.day.cq.commons.ImageHelper;
import com.day.cq.wcm.foundation.Image;
import com.day.cq.wcm.commons.AbstractImageServlet;
import com.day.image.Layer;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;

public class img_GET extends AbstractImageServlet {
    protected Layer createLayer(ImageContext c) throws RepositoryException, IOException {
        return null;
    }

    protected void writeLayer(SlingHttpServletRequest req, SlingHttpServletResponse resp, ImageContext c, Layer layer)
                                throws IOException, RepositoryException {
        Image image = new Image(c.resource);
        image.setItemName(Image.NN_FILE, "image");
        image.setItemName(Image.PN_REFERENCE, "imageReference");

        if (!image.hasContent()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        layer = image.getLayer(false, false,false);

        String rUri = req.getRequestURI();
        String ratio = rUri.substring(rUri.lastIndexOf("/") + 1, rUri.lastIndexOf(".jpg"));
        String cords = c.properties.get(ratio, "");

        boolean modified = false;

        if(!"".equals(cords)){
            Rectangle rect = ImageHelper.getCropRect(cords, c.resource.getPath());
            layer.crop(rect);

            modified = true;
        }else{
            modified = image.crop(layer) != null;
        }

        modified |= image.resize(layer) != null;
        modified |= image.rotate(layer) != null;

        if (modified) {
            resp.setContentType(c.requestImageType);
            layer.write(c.requestImageType, 1.0, resp.getOutputStream());
        } else {
            Property data = image.getData();
            InputStream in = data.getStream();
            resp.setContentLength((int) data.getLength());
            String contentType = image.getMimeType();

            if (contentType.equals("application/octet-stream")) {
                contentType=c.requestImageType;
            }

            resp.setContentType(contentType);
            IOUtils.copy(in, resp.getOutputStream());
            in.close();
        }

        resp.flushBuffer();
    }
}

9) Finally, add the following properties in your CRX node /apps/imagecustomar/image/dialog/items/img





Configuring CQ 56 war on Tomcat and run it from Intellij IDEA

Goal


This post is for people who do not like opening too many windows while coding. To run AEM (CQ) instance the general approach followed by developers is, double click the CQ quickstart jar or open a command prompt window and run the jar using "java" command. In both cases a new window opens and you obviously will have your IDE ( Eclipse or Intellij ) open for coding. So,

1) A CQ run window
2) Eclipse or Intellij IDEA IDE application window
3) Browser to test your application

The intention of this post is to get rid of CQ window and run the application from your IDE; also explains the CQ configuration on Tomcat


Install and Configure


1) Download & Install IntelliJ IDEA 12; Download Tomcat 7 zip and expand it to a directory, say C:\dev\code\install\apache-tomcat-7.0.42

2) Open C:\dev\code\install\apache-tomcat-7.0.42\bin\catalina.bat and add enough memory

                       set JAVA_OPTS=-Xmx1792m -XX:MaxPermSize=512m

3) Open C:\dev\code\install\apache-tomcat-7.0.42\conf\server.xml and change the http connector port from 8080 to CQ author instance default port 4502

4) Delete ROOT war from C:\dev\code\install\apache-tomcat-7.0.42\webapps to deploy CQ war as the root application

5) Copy cq-quickstart-5.6.0-20130125.war to C:\dev\code\install\apache-tomcat-7.0.42\webapps

6) Rename cq-quickstart-5.6.0-20130125.war to ROOT.war

7) Create the CRX folder C:\dev\code\install\apache-tomcat-7.0.42\crx-quickstart. This is the location of your JCR repository created by CQ

8) Using WinZIP or WinRAR, extract the file web.xml in ROOT.war (ROOT.war\WEB-INF\web.xml) onto the filesystem.Open extracted web.xml in a text editor and change init-param sling.home from crx-quickstart to C:/dev/code/install/apache-tomcat-7.0.42/crx-quickstart

        <init-param>
        <param-name>sling.home</param-name>
        <param-value>C:/dev/code/install/apache-tomcat-7.0.42/crx-quickstart</param-value>
        </init-param>

9) Open cmd prompt at C:\dev\code\install\apache-tomcat-7.0.42\bin and start server using the command "catalina run". Just to make sure everything is alright, this is the first and last time we'll be doing it

10) Check the folder C:\dev\code\install\apache-tomcat-7.0.42\crx-quickstart and the following folders should have be created

             launchpad
             logs
             repository

11) It may take 3-4 minutes first time, for the server to start and get to a running state, wait and check the log C:\dev\code\install\apache-tomcat-7.0.42\crx-quickstart\logs\error.log to see if all services are starting up properly

12) Open a browser window and type http://localhost:4502 and it should redirect to http://localhost:8080/system/granite/license/index.html for license information. If you have license.properties get the customer name and license key from file, and enter in the form

13) Check http://localhost:4502/system/console/bundles and http://localhost:4502/crx/de to see if everything is ok ( user/pass : admin/admin )


Starting Tomcat from IntelliJ IDEA


1) Shutdown the tomcat started in section above

2) In your Intellij 12, open Run -> Edit Configurations

3) Click "+" and add Tomcat Server -> Local

4) Enter the following information in "Server" tab

                 Name: Tomcat 7
                 Application Server: Click configure and select the Tomcat Home (C:\dev\code\install\apache-tomcat-7.0.42)
                 Startup page: http://localhost:4502/
                 Http Port: 4502

5) Click on tab "Deployment", click "+" -> "External Source" and select the CQ ROOT.war. So, when tomcat is started the ROOT war (CQ) is deployed

6) Click on tab "Startup/Connection". Developers generally start the server in debug mode, so select "Debug" and add necessary settings or just select default settings and click ok.

7) Start the server from your IDE by clicking on Run -> Debug 'Tomcat 7' and you should see the ROOT war being deployed in server startup messages




Creating Custom Page Properties Dialog in CQ

Goal


In this post, we create a custom page properties dialog with fields to store additional page information and display it on the page. A custom page properties dialog is useful when you'd like to add project specific page properties and make use of them in the components added on page. Let us also add a simple listener on a page properties field. Source code available for download

Package Install


First Steps


If you are new to developing applications on CQ, i recommend reading the following posts and develop a sample component before you continue...

1) A Developer Installation
2) Developing a sample CQ Page Component


Create the Panel


1) Assuming you have the following page component created and available in CRX



2) Right click on /apps/samples/components/basictemplate, select "Create Node", enter the following details and click Ok

                        Name: customprops
                        Type: cq:TabPanel

3) Add other necessary details for creating the tab panel

                         xtype: tabpanel
                         title: Custom Properties
                         activeTab: 0

4) Save changes in CRX

5) Right click on /apps/samples/components/basictemplate/customprops, select "Create Node", enter the following details and click "Ok"

                    Name: items
                    Type: cq:WidgetCollection

6) Save Changes

7) Follow the same process to create node with name details of type cq:Widget ( /apps/samples/components/basictemplate/customprops/items/details ) with the following details

                xtype: panel
                title: Details

8) Create node items of type cq:WidgetCollection (/apps/samples/components/basictemplate/customprops/items/details/items)

9) Create node message of type cq:Widget (/apps/samples/components/basictemplate/customprops/items/details/items/message), enter the following details and Save changes

                  xtype: textfield
                  fieldLabel: Welcome Message
                  name: ./title
                  defaultValue: Welcome to CQ



10) At this point we've created a Custom Properties dialog Tab Panel with a Text Field to enter some text

Add Panel to SideKick


1) To add the panel to sidekick, Create a file head.jsp ( Right click on /apps/samples/components/basictemplate, Select "Create" -> "Create File" ), Save changes.

2) Checkout the file head.jsp to your IDE. ( I use Intelij IDEA IDE, See this post on how to integrate your IDEA with CRX )

3) Add the following code in your basictemplate head.jsp created above ( These are pieces from /libs/foundation/components/page/head.jsp, /libs/wcm/core/components/init/init.jsp to load the side kick, /libs/foundation/components/page being the sling:resourceSuperType of our basictemplate component )

<%@include file="/libs/foundation/global.jsp"%>
<%@page import="com.day.cq.wcm.api.WCMMode,
                com.day.cq.widget.HtmlLibraryManager,org.apache.commons.lang.StringEscapeUtils"
%>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0"%>



<head>
    
    <%
        if ( WCMMode.fromRequest(request) != WCMMode.DISABLED ) {
            HtmlLibraryManager htmlMgr = sling.getService(HtmlLibraryManager.class);

            if (htmlMgr != null) {
                htmlMgr.writeCssInclude(slingRequest, out, "cq.wcm.edit", "cq.tagging", "cq.security");
                htmlMgr.writeJsInclude(slingRequest, out, "cq.wcm.edit", "cq.tagging", "cq.security" );
            }

            String dlgPath = null;

            if ( ( editContext != null ) && ( editContext.getComponent() != null ) ) {
                dlgPath = editContext.getComponent().getDialogPath();
            }
    %>

    

    <%
        }
    %>

    <%= StringEscapeUtils.escapeXml(currentPage.getTitle()) %>
</head>

4) Check-in the file to CRX ( or instead of using IDE you can use CRXDE Lite in browser, open head.jsp, add the above code and save changes)

5) Assuming you have created page "My Page" of type "basictemplate" under http://localhost:4502/siteadmin#/content -> FirstApp Demo Site; Access the url http://localhost:4502/cf#/content/firstapp-demo-site/my-page.html, goto SideKick -> Page tab, at the bottom you should see Custom Page Properties. Click and the tab panel we added earlier is displayed in a window



6) Enter "Thank you" in the text field and click ok; the page gets refreshed

7) Open CRXDE Lite (http://localhost:4502/crx/de) and goto /content/firstapp-demo-site/my-page/jcr:content; the title textfield message you added above should have been saved as the property title




8) Let us modify the basictemplate body.jsp (/apps/samples/components/basictemplate/body.jsp) to show this title on the page. Add the following code to body.jsp and check-in to CRX

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

<cq:defineobjects/>

<%
    //properties is an implicit object defined in cq taglib and made available
    //with tag declaration <cq:defineobjects/>
    String myTitle = String.valueOf(properties.get("title"));
    pageContext.setAttribute("myTitle", myTitle);
%>

${myTitle} message was added in SideKick -> Page tab -> Custom Page Properties

9) The "Thank you" message added earlier in "Custom Page Properties" is shown




Add ExtJS Listener on Panel


1) Let's add a sample JS listener on the panel 

2) Open CRXDE Lite, goto /apps/samples/components/basictemplate/customprops, right click and add node listeners of type nt:unstructured. Add the following details

                    afterrender: function(dialog){ myTitleFn(dialog); }



Here we are adding a listener for the afterrender event on tab panel and listener function calls the function myTitleFn with dialog as argument. In the next step we are going define the function "myTitleFn"

3) Define the myTitleFn global function in your basictemplate body.jsp. It's not a best practice to define global functions, always scope the functions in JS objects. We are doing it to keep things simple, the function shows an alert with dialog title.

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

<cq:defineObjects/>

<%
    //properties is an implicit object defined in cq taglib and made available
    //with tag declaration <cq:defineObjects/>
    String myTitle = String.valueOf(properties.get("title"));
    pageContext.setAttribute("myTitle", myTitle);
%>

${myTitle} message was added in SideKick -> Page tab -> Custom Page Properties

<script type="text/javascript">
    var myTitleFn = function(dialog){
        alert("Please enter your welcome message in '" + dialog.title + "'");
    }
</script>

4) Check-in or Save body.jsp in CRX, access page http://localhost:4502/cf#/content/firstapp-demo-site/my-page.html, Click on SideKick -> Page tab -> Custom Page Properties and you should see the alert




 5) Download the source code

Coding a Sample Auto Tag AEM 56 Bundle on IntelliJ IDEA 12

Goal


This post is about creating a sample OSGI bundle (what is an OSGI bundle? Any search engine will bring you loads of information) from scratch on IntelliJ IDEA 12. Read this good article on CQ listeners

Here we create a bundle with


2) When a new page is created, listener automatically creates a new tag with page name and assigns it to the page. So basically some sample code to create and assign tags.


Install


Download and Install AEM (Day CQ) 561, IntelliJ IDEA 12 and Maven 310


Setup Maven


1) To add the dependency jars to your IDE, install and setup Maven

2) Download Maven and extract it to C:\dev\apache-maven-3.1.0.

3) Open System Environment Variables and add the System Varilable M2_HOME=C:\dev\apache-maven-3.1.0

4) Add the User Variable M2=%M2_HOME%\bin


New IDEA Module


1) In your IDEA project, Select File -> New Module -> Maven Module and enter the module name autotag



2) Enter the following 

                     GroupId: com.mysample.autotag
                     ArtifactId: autotag
                     Version: 1.0

3) Check "Create from archetype" and click "Add archetype"


4) Enter the following information and click ok

                        GroupdId: com.day.jcr.vault
                        ArtifactId: multimodule-content-package-archetype
                        Version: 1.0.2
                        Repository: http://repo.adobe.com/nexus/content/groups/public




5) Add the following maven properties

                        appsFolderName - autotag
                        artifactName - Auto Tag Listener 
                        packageGroup - Test Company



6) Here is a screen shot with all the details entered. Click Finish



7) Here is my autotag module in IDEA



8) Maven dependencies were already imported and created on file system. For example, the dependency "org.osgi"



On file system, the jars are available in 



9) Lets delete the unnecessary source files created by archetype; later we'll add our listener code. At the time of this writing i deleted files under "C:\dev\code\projects\autotag\bundle\src\main\java\com\mysample\autotag". Here is my module now with "C:\dev\code\projects\autotag\bundle\src\main\java" added as module source




Sample Listener Logic


1) Lets create a folder listeners and add some sample listener code. Here we are adding a JCR observation listener. This listener operates at JCR level and so has no knowledge of sling web framework, we operate on the raw jcr properties.





2) Here is the listener source code. The annotation "@Reference" for "repository" makes the jcr repository available for querying nodes. "activate" and "deactivate" methods are self-explanatory. 

Line 31: we are registering the listener for "NODE_ADDED" events only. So when a new page is created, a node is added in JCR under "/content"; the NODE_ADDED event fires and this listener catches execution.

package com.mysample.autotag.listeners;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;

@Component
public class AutoTagListener implements EventListener {
    private final Logger LOGGER = LoggerFactory.getLogger(AutoTagListener.class);

    @Reference
    private SlingRepository repository;

    private Session session;
    private ObservationManager observationManager;

    protected void activate(ComponentContext context) throws Exception {
        session = repository.loginAdministrative(null);
        observationManager = session.getWorkspace().getObservationManager();

        observationManager.addEventListener(this, Event.NODE_ADDED, "/", true, null,
                null, true);
        LOGGER.info("Added JCR event listener - AutoTagListener");
    }

    protected void deactivate(ComponentContext componentContext) {
        try {
            if (observationManager != null) {
                observationManager.removeEventListener(this);
                LOGGER.info("Removed JCR event listener - AutoTagListener");
            }
        } catch (RepositoryException re) {
            LOGGER.error("Error removing the JCR event listener - AutoTagListener", re);
        } finally {
            if (session != null) {
                session.logout();
                session = null;
            }
        }
    }

    public void onEvent(EventIterator it) {
        try {

        }catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

        return;
    }
}


Install and Test Bundle


1) In your IDEA click on "Maven Projects" on the right side of your screen




2) Under Profiles -> Check "autoInstallBundle", "autoInstallPackage". Under "Reactor Project" right click on "install' and "Run Maven Build". Make sure your AEM (CQ) instance is up and running before you attempt this.




3) If the install is successful, in CRX Lite you can see the installed component bundle



4) Log "C:\dev\code\install\author\crx-quickstart\logs\error.log" shows the autotag bundle installed



5) Bundles console "http://localhost:4502/system/console/bundles", confirms the bundle installation



The Real Listener


1) Lets add some useful logic in the listener. The logic we are going to add creates a tag with page name in CRX and assigns it to the page

2) Add the following dependency to C:\dev\code\projects\autotag\pom.xml
            
                com.day.cq.tagging
                cq-tagging
                5.6.2
            
3) Add the following dependency to C:\dev\code\projects\autotag\bundle\pom.xml
        
            com.day.cq.tagging
            cq-tagging
        

The jars for necessary dependency are imported to "C:\Users\nalabotu\.m2\repository\com\day\cq\tagging\cq-tagging\5.6.2". If the jar is not available, get it from CRX (/libs/cq/tagging/install/cq-tagging-5.6.2.jar) and place it in the location

2) Add necessary logic to listener

Line 25: Declare a variable for tag namespace. Namespaces are like containers for tags. Tags that are going to be created by this listener are stored in the namespace "mysample"

Line 30, 31: Create a reference to tag manager implementation (a tag manager object for playing with tags)

Line 67, 74: A node added event is fired when a page content node is created. So we get the page content node, page node and continue with tag creation. If the node created is not of our interest (say an audit node) we skip the event processing

Line 85: Check if the namespace exists. A null means the namespace doesn't exist, so create one

Line 90: Create the tag with page name

Line 95: Create the cq:tags property on page content node and set it with the tag created above.

Line 96: Very important, save the changes

package com.mysample.autotag.listeners;

import com.day.cq.tagging.JcrTagManagerFactory;
import com.day.cq.tagging.Tag;
import com.day.cq.tagging.TagManager;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;

@Component
public class AutoTagListener implements EventListener {
    private final Logger LOGGER = LoggerFactory.getLogger(AutoTagListener.class);

    private static final String NAMESPACE = "/etc/tags/mysample";

    @Reference
    private SlingRepository repository;

    @Reference
    JcrTagManagerFactory tmf;

    private Session session;
    private ObservationManager observationManager;

    protected void activate(ComponentContext context) throws Exception {
        session = repository.loginAdministrative(null);
        observationManager = session.getWorkspace().getObservationManager();

        observationManager.addEventListener(this, Event.NODE_ADDED, "/", true, null,
                null, true);
        LOGGER.info("Added JCR event listener - AutoTagListener");
    }

    protected void deactivate(ComponentContext componentContext) {
        try {
            if (observationManager != null) {
                observationManager.removeEventListener(this);
                LOGGER.info("Removed JCR event listener - AutoTagListener");
            }
        } catch (RepositoryException re) {
            LOGGER.error("Error removing the JCR event listener - AutoTagListener", re);
        } finally {
            if (session != null) {
                session.logout();
                session = null;
            }
        }
    }

    public void onEvent(EventIterator it) {
        try {
            while (it.hasNext()) {
                Event event = it.nextEvent();
                LOGGER.info("AutoTagListener - new add event: ", event.getPath());

                Node pageContentNode = session.getNode(event.getPath());

                if( ( pageContentNode == null ) || !pageContentNode.getPrimaryNodeType().isNodeType("cq:PageContent")){
                    LOGGER.debug("Skip processing node: " + event.getPath());
                    return;
                }

                Node pageNode = pageContentNode.getParent();

                if( ( pageNode == null ) || !pageNode.getPrimaryNodeType().isNodeType("cq:Page")){
                    LOGGER.debug("Skip processing node: " + pageNode);
                    return;
                }

                TagManager tMgr = tmf.getTagManager(session);
                Tag superTag = tMgr.resolve(NAMESPACE);
                Tag tag = null;

                if(superTag == null){
                    tag = tMgr.createTag(NAMESPACE, "My Sample", "My Sample tags", true);
                    LOGGER.info("Tag Name Space created : ", tag.getPath());
                }

                tag = tMgr.createTag(NAMESPACE + "/" + pageNode.getName(), pageNode.getName(), "Auto tag : " + pageNode.getName(), true);

                String tagArray[] = new String[1];
                tagArray[0] = tag.getNamespace().getName() + ":" + tag.getPath().substring(tag.getPath().indexOf(NAMESPACE) + NAMESPACE.length() + 1);

                pageContentNode.setProperty("cq:tags", tagArray);
                session.save();
            }
        }catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

        return;
    }
}

3) Reinstall the component (Run Maven Goal "install" )

If you see the following error in error.log

02.09.2013 17:08:21.097 *WARN* [127.0.0.1 [1378159701028] POST /crx/packmgr/service.jsp HTTP/1.1] com.day.jcr.vault.packaging.impl.JcrPackageImpl Refusing to recreate snapshot Test Company/.snapshot:autotag-content:1.0, already exists.

Remove the previously installed bundle from OSGI container (http://localhost:4502/system/console/bundles)



Remove the previously installed component from CRX (http://localhost:4502/crx/de/index.jsp#/apps, Right click on "autotag" and "Delete")





4) Run the maven goal "install"

Test and Check


1) Create a page "Listener Test" of any template type in siteadmin console (http://localhost:4502/siteadmin)



2) Check the Tagging console (http://localhost:4502/tagging). The tag listener-test should have been created under namespace "My Sample"



3) In CRX (http://localhost:4502/crx) check the tags node (/etc/tags/mysample/listener-test)



4) Check the page "Listener Test" properties in CRX




5) Check if tag "listener-test" is assigned to the page by opening Side Kick -> Pages tab -> Page Properties -> Tags/Keywords



6) Check if the tag is assigned by navigating to the page (http://localhost:4502/cf#/content/test-site/listener-test.html) and searching with tags:listener-test in Content Finder Pages tab