Tuesday, April 29, 2014

EditConfig tips

A couple of months ago I read very good article from Dan Klco about EditConfigs. Some weeks later I had to practice it myself, so here are some tips from my side.

1) Custom listeners. Some of my components needed to execute javascripts after their configuration is changed. There are build-in listeners shortcuts:

  • REFRESH_PAGE, 
  • REFRESH_PARENT, 
  • REFRESH_SELF, 
  • REFRESH_SELFMOVED, 
  • REFRESH_INSERTED
Most important and widely used are first three. Refreshing whole page is generally a bad idea as it's a bad user experience. Ideally you should use refresh_parent or refresh_self and only a part of the page will be updated via ajax call.
In my case it was not enough to use these shortcuts, because apart from refreshing a parent container, some page-scoped javascripts need to be executed. Therefore I had to write my own listener.

<?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"
          cq:actions="[text:My Component,EDIT,DELETE,COPYMOVE]"
          cq:layout="editbar"
          jcr:primaryType="cq:EditConfig"
          cq:disableTargeting="{Boolean}true"
          cq:dialogMode="floating">
    <cq:listeners
            jcr:primaryType="cq:EditListenersConfig"
            afterinsert="function(path, definition) { CQWidgets.refreshParent(this); }"
            afteredit="function(path, definition) { CQWidgets.refreshParent(this); }"/>
</jcr:root>

where CQWidgets is a separate JS file which is included inside clientlib in edit mode.

var CQWidgets = function () {

    return {
        refreshParent: function (editBar) {
            editBar.refreshParent();
            MyComponentController.initializeSliders(); // this is where my javascript logic is being executed
        }
    }
}();


2) EditConfig actions.
<TBD>

Content Finder Visibility

By default Content Finder is visible only for the following paths:

  • "/content/*",
  • "/etc/scaffolding/*",
  • "/etc/workflow/packages/*"

You can check permissions for each individual tab inside Content Finder here - "/libs/wcm/extensions/contentfinder".

Sometimes you need to have administration page of your application which usually resides under /etc/<your_app_name> folder. Therefore you will NOT see content finder in edit mode.
In order for it to appear there you will have to either customize existing scripts (by copying it under /apps) or - better way - you will create special folder in your application which has properties extensionType=contentfinder_extension and extensionGroup=tabs. It tells ContentFinder manager to include scripts under this folder as separate tabs.

I personally prefer to keep folder structure similar to original CQ hierarchy, so inside my application I create folder hierarchy:
<app_folder>
    wcm
        extensions
            contentfinder (which has properties above)

Under contentfinder I will then add my own tabs. They can be the same as default ones (f.e. images.js) or include my own custom logic. In both cases it is required to set allowedPaths property inside these scripts which points to your page's path. For example:

    "allowedPaths": [
        "/etc/myproject/*"
    ]

Sidekick Customization

Sometimes it is necessary to override default behaviour of CQ Sidekick. For example, you need to reload the page when author presses "Preview" button to apply some javascript logic or rename buttons in Sidekick, etc.

There are two ways to do it:
1). Straightforward (ugly). You need to copy script init.jsp from "/libs/wcm/core/components/init" directly under your application folder, customize it and include it from your page. (You can also copy this script under /apps but that would affect all other applications on this CQ instance).
Let's reload the page when author clicks on Preview button.

For that we need to change these lines:
    CQ.WCM.launchSidekick("<%= currentPage.getPath() %>", {
        propsDialog: "<%= dlgPath == null ? "" : dlgPath %>",
        locked: <%= currentPage.isLocked() %>,
        previewReload: "true"
    });
Notice previewReload property, it does the trick.

Next we need to include this script from our page component:
    <cq:include script="/apps/skywalker/init.jsp" />

2) Programmatic (preferrable approach).
Inside your page component please use the following code:

<script>
    function checkSidekickStatus() {
        if (CQ.WCM.isSidekickReady()) {
            CQ.WCM.getSidekick().previewReload = true;
            clearTimeout(timeout);
        }
    }
    var timeout = setInterval(checkSidekickStatus, 1000);
</script>

In other words, we wait until sidekick is loaded and then modify its properties. In this case we set previewReload to true. To check all available properties of sidekick please visit http://dev.day.com/docs/en/cq/current/widgets-api/index.html and search for "sidekick" there.

Friday, October 18, 2013

Monday, August 19, 2013

Creating custom JCR node type

Sometimes it is required for application to have custom schema with application-specific namespace, node types and properties. Although David's model rule #1 is "Data First, Structure Later. Maybe", sometimes "maybe" becomes "must". So how I can create custom node type?

First of all, read carefully official JCR documentation: Node Types and Examples. ;-) And now let's implement it.

I. Create node type definition using CND descriptor. 

For simplicity we will put it into existing CQ namespace. (If you want to customize namespace, just define it alongside existing namespaces in CND file).

<cq  = 'http://www.day.com/jcr/cq/1.0'>
<sling = 'http://sling.apache.org/jcr/sling/1.0'>

//---- Custom Node Type ----

[cq:CustomNode] > nt:hierarchyNode, mix:title, mix:modified, mix:versionable
    orderable
    - * (undefined) multiple
    - * (undefined)
    + * (nt:base) = cq:CustomNode version

Here we define node type, which is hierarchical, contains jcr:created, jcr:modified properties and which supports versioning (mix:versionable). If your project is built using maven, put this file to the resources folder of your OSGI bundle. I use CQ-style and put it under CQ-INF/nodetypes folder.

II. Create OSGI listener which registers our node type during bundle activation.

Now you need to register newly created definition. For that you should invoke JCR API from the links above. In order to simplify things, we will register our node type during bundle activation, so we need to create an OSGI component.

package com.yvv.customnode.bundle;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.jackrabbit.api.JackrabbitNodeTypeManager;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import java.io.InputStream;

@Component
public class CustomNodeTypeListener {

    private final Logger LOGGER = LoggerFactory.getLogger(CustomNodeTypeListener.class);
    public static final String NODE_TYPE_NAME = "cq:CustomNode";

    @Reference
    private org.apache.sling.jcr.api.SlingRepository repository;

    private Session session;

    protected void activate(ComponentContext context) throws Exception {
        session = repository.loginAdministrative(null);
        registerCustomNodeTypes(session);
    }

    protected void deactivate(ComponentContext componentContext) {
        if (session != null) {
            session.logout();
            session = null;
        }
    }

    public void registerCustomNodeTypes(Session session)
            throws Exception {
        JackrabbitNodeTypeManager manager = (JackrabbitNodeTypeManager) session.getWorkspace().getNodeTypeManager();
        if (manager.hasNodeType(NODE_TYPE_NAME)) {
            manager.unregisterNodeType(NODE_TYPE_NAME);
        }
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("CQ-INF/nodetypes/cq-customnode.cnd");
        manager.registerNodeTypes(is, JackrabbitNodeTypeManager.TEXT_X_JCR_CND);
    }
}

III. Verify newly created node type.

Browse to http://localhost:4502/crx/explorer/nodetypes/index.jsp and under the tab "All registered node types" find cq:CustomNode, open it to view details.

IV. Create nodes using your custom type.

You can do it using i.e. curl command:
curl -D -u admin:admin -F"jcr:primaryType=cq:CustomNode" -F"title=some title" http://localhost:4502/content/customnode

Tuesday, August 6, 2013

Reporting using Groovy scripts

It's often necessary to get different kinds of statistics on CQ data. You can do it in several ways:
  • using build-in CQ reporting engine . Unfortunately it is pretty inflexible and requires lot's of configuration to be done, so I would recommend using it for simple JC queries only;
  • writing custom components with required logic in ESP/JSP/Java/Groovy
  • use third-party tools like Splunk which parses CQ logs (you should have required data in logs! ;-)  

In this blog post I focus on groovy scripts as one of the most quick ways to operate with CQ API.
To use groovy scripts you can use pretty nice groovy console, which is ultimately installed as a CQ package.

As an example, let's write a simple script which will gather DAM statistics (# of all assets in JCR repository, total DAM storage size, asset name, asset path, asset file size).

First you need to install groovy console according to instructions from https://github.com/Citytechinc/cq-groovy-console
Once installed, navigate to http://localhost:4502/groovyconsole. You should see code area in the center of the screen, Type the following code there:
import javax.jcr.query.*
import com.day.cq.dam.api.*
import javax.jcr.*
import org.apache.sling.api.resource.*

def query = createSQL2Query("/content/dam") // TODO start folder for search
def result = query.execute()
def rows = result.rows

totalSize = 0
rows.each { row ->
    Resource res = resourceResolver.getResource(row.path)
    Asset asset = res.adaptTo(Asset.class)
    fileSize = asset.getOriginal().getSize();
    totalSize += fileSize
//    println "Name=$asset.path; fileSize=$fileSize"
}
println "Total assets number: ${rows.size}; Total file size: ${formatFileSize(totalSize)}"


def createSQL2Query(startPage) {
    def queryManager = session.workspace.queryManager

    def statement = "select * from [nt:base] as p where (isdescendantnode (p, '$startPage')) and p.[jcr:primaryType] = 'dam:Asset'"

    def query = queryManager.createQuery(statement, Query.JCR_SQL2)

    query
}

// Convert a number of bytes to a more readable string
def formatFileSize(size) {
    labels = [ ' bytes', 'KB', 'MB', 'GB', 'TB' ]
    label = labels.find {
        if( size < 1024 ) {
            true
        } else {
            size /= 1024
            false
        }
    }
    "${new java.text.DecimalFormat('0.##').format( size )}$label"
}
After pressing "Run Script" button you will see output below console. You can of course substitute println to SLF4J methods, here it's just for brevity.


Friday, August 2, 2013

CQ Page Editing Tips

Here is the summary of several tips useful during developing your CQ pages (it's an aggregated information from other blogs, just to keep it in one place):

1. Show content finder on the page.
Add /cf# prefix to page's URL. Remove it to hide content finder.

Example: http://localhost:4502/cf#/content/geometrixx/en/products.html


2. Set the mode of your page.
Add parameter wcmmode=(edit|preview|design|disabled)

Example: http://localhost:4502/content/geometrixx/en/products.html?wcmmode=preview


3. Shows details of all the components used on your page.
Add parameter debug=layout


Example: http://localhost:4502/content/geometrixx/en/products.html?debug=layout



4. Run Firebug Lite tool for the web page. 
Add parameter debugConsole=true


Example: http://localhost:4502/content/geometrixx/en/products.html?debugConsole=true


5. Writes Logs all data in your Clientlib category as separate files (to view it you should view HTML-source of the page).
Add parameter debugClientLibs=true



Example: http://localhost:4502/content/geometrixx/en/products.html?debugClientLibs=true





6. View timing information of your page.
Press CTRL+SHIFT+U