Author Archive

October 4, 2013

Custom Component: Manual List Widget

Requirement: The users want the ability to manually choose a list of of items from a source list of items.

Example: A list of all the articles in the system is the source list and the users want to select the most important ones to be listed on the home page of a site.

To achieve this, I created a custom ExtJs widget.

  • The users are able to double click on an item on the left (source) list to move the said item to the right list
  • They can manually order the items on the right list via drag and drop.
  • They can double click the item on the right list to remove it from the list
  • They are also able to pass tags to limit the results (your custom servlet that returns the JSON object to be consumed by the widget will need to handle this use case)

Below are the steps to use this widget in your implementation.

1. Paste the widget code from below into a JS file in your CQ implementation.

//Create a new class based on existing CompositeField
CQ.form.GridtoGrid = CQ.Ext.extend(CQ.CustomContentPanel, {
    secondGrid: null,
    firstGrid: null,
    columnModel: null,
    reader: null,
    layout: 'fit',
    constructor : function(config){

        var dataSrcUrl;

        if( config.dataSrcUrl != null ){
            dataSrcUrl = config.dataSrcUrl;
        }

        var jsonReaderConfig;
        if( config.jsonReaderConfig != null ){
            jsonReaderConfig = config.jsonReaderConfig;
        }

        var sortDirection;
        if( config.sortDirection != null ){
            sortDirection = config.sortDirection;
        }

        var sortField;
        if( config.sortField != null ){
            sortField = config.sortField;
        }

        var columnModelConfig;
        if( config.columnModelConfig != null ){
            columnModelConfig = config.columnModelConfig;
        }

        console.log( dataSrcUrl );
        console.log( sortDirection );
        console.log( sortField );
        console.log( CQ.Ext.util.JSON.decode( jsonReaderConfig ) );
        console.log( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        var proxy = new CQ.Ext.data.HttpProxy( {url:  dataSrcUrl} );

        reader = new CQ.Ext.data.JsonReader( {}, CQ.Ext.util.JSON.decode( jsonReaderConfig ) );

        var sourceStore = new CQ.Ext.data.Store(
            {
                proxy: proxy,
                sortInfo: {
                    field: sortField,
                    direction: sortDirection
                },
                reader: reader
            }
        );
        sourceStore.load();

        columnModel = new CQ.Ext.grid.ColumnModel( CQ.Ext.util.JSON.decode( columnModelConfig ) );

        firstGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){

                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = firstGrid.getStore().getAt( index );

                    //Add the record to the destinationStore.
                    secondGrid.getStore().add( moveRecord );
                }
            },
            store            : sourceStore,
            name             : 'sourceGrid',
            colModel          : columnModel,
            stripeRows       : true,
            title            : 'Latest Articles',
            margins          : '0 2 0 0',
            x: 0,
            y:100,
            height: 500

        });

        //destination store
        var destinationStore = new CQ.Ext.data.Store({
            reader: reader
        });

        //destination grid
        secondGrid = new CQ.Ext.grid.GridPanel({
            listeners: {
                rowdblclick: function (grid, index){
                    //On DoubleClick, get the record from the sourceStore using the index.
                    var moveRecord = secondGrid.getStore().getAt( index );

                    //Remove the record from the destinationStore.
                    secondGrid.getStore().remove( moveRecord );
                }
            },
            name: 'destinationGrid',
            store: destinationStore,
            colModel: columnModel,
            stripeRows       : true,
            title            : 'Displayed Articles',
            enableDragDrop: true,
            hideHeaders: true,
            ddGroup: 'ddGroup',
            margins          : '0 0 0 3',
            x: 450,
            y: 100,
            height: 500
        });

        //Set the dropZone for the second grid so that users can manually re-order the list.
        secondGrid.on('render', function() {

            secondGrid.dropZone = new CQ.Ext.dd.DropZone(secondGrid.getView().scroller, {
                ddGroup: 'ddGroup',

                //If the mouse is over a grid row, return that node. This is
                //provided as the "target" parameter in all "onNodeXXXX" node event handling functions
                getTargetFromEvent: function(e) {
                    return e.getTarget(secondGrid.getView().rowSelector);
                },

                //On entry into a target node, highlight that node.
                onNodeEnter : function(target, dd, e, data){
                    CQ.Ext.fly(target).addClass('my-row-highlight-class');
                },

                //On exit from a target node, unhighlight that node.
                onNodeOut : function(target, dd, e, data){
                    CQ.Ext.fly(target).removeClass('my-row-highlight-class');
                },

                //While over a target node, return the default drop allowed class which
                //places a "tick" icon into the drag proxy.
                onNodeOver : function(target, dd, e, data){
                    return CQ.Ext.dd.DropZone.prototype.dropAllowed;
                },

                //On NodeDrop event
                onNodeDrop : function(target, dd, e, data){

                    var targetRowIndex = secondGrid.getView().findRowIndex(target);
                    var sourceRowIndex = data.rowIndex;

                    if( targetRowIndex == false ){
                        return false;
                    }

                    if( targetRowIndex == sourceRowIndex ){
                        return false;
                    }

                    var ds = secondGrid.getStore();

                    var sourceRecord = ds.getAt(sourceRowIndex);

                    ds.insert( targetRowIndex, sourceRecord );

                    //Reconfigure the destination list to new store.
                    secondGrid.reconfigure ( ds, columnModel );

                    return true;
                }
            });
        })

        //Tag Inputbox
        var tagInputField = new CQ.tagging.TagInputField({
            listeners:{
                addtag: function( inputField, tag ){

                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var tagsIndex = proxyUrl.indexOf( '?tags=' );

                    if( tagsIndex > -1 ){
                        proxyUrl = proxyUrl + "," + tagId;
                    } else {
                        proxyUrl = proxyUrl + "?tags=" + tagId;
                    }
                    //alert( "Add Tag: " + proxyUrl );
                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                },
                removetag: function( inputField, tag ){
                    var proxyUrl = firstGrid.getStore().proxy.url;
                    var tagId = tag.tagID;

                    var splitUrl = new Array();
                    splitUrl = proxyUrl.split( '?tags=' );

                    //alert( "Remove Tag 1 : " + proxyUrl );

                    if( splitUrl[1].indexOf( "," + tagId ) > -1 ){
                        splitUrl[1] = splitUrl[1].replace( "," + tagId, "" );
                    } else {
                        splitUrl[1] = splitUrl[1].replace( tagId , "" );
                    }

                    if( splitUrl[1].indexOf( ',' ) == 0 ){
                        splitUrl[1] = splitUrl[1].substring(1);
                    }

                    if( splitUrl[1] != null && splitUrl[1].length > 0 ){
                        proxyUrl = splitUrl[0] + '?tags=' + splitUrl[1];
                    } else {
                        proxyUrl = splitUrl[0];
                    }
                    //alert( "Remove Tag 2 : " + proxyUrl );

                    var updatedSourceStore = new CQ.Ext.data.Store({
                        url: proxyUrl,
                        reader: reader
                    });

                    updatedSourceStore.load();
                    firstGrid.reconfigure( updatedSourceStore, columnModel );
                }

            },
            title: 'Tag Properties',
            name: './tags',
            x: 250,
            y: 0,
            width: 400
        });
        CQ.form.GridtoGrid.superclass.constructor.call(this, config);

        this.selectionForm = new CQ.Ext.Panel({
            name         : this.name,
            width        : 'auto',
            height       : 600,
            border       : false,
            layout       : {
                type: 'absolute',
                padding: 0
            },
            items        : [
                tagInputField,
                firstGrid,
                secondGrid
            ]
        });
        this.add(this.selectionForm);

    },
    submitPanel: function() {

        var store = secondGrid.getStore();
        var storeData = store.data;
        var storeFields = store.fields;
        var params = {};

        if( storeData.length > 0 ){
            for(var i = 0; i < storeData.length; i++) {
                for(var j = 0; j < storeFields.length; j++){
                    var name = storeFields.item(j).name;
                    if (!params[name]) {
                        params[name] = new Array();
                    }

                    if( name == 'headline' ){
                        var headline = storeData.get(i).get(name);
                        if( headline.indexOf ( '_' ) == headline.length - 1 ){
                            params[name] = params[name].concat( storeData.get(i).get(name));
                        } else {
                            params[name] = params[name].concat( storeData.get(i).get(name) + "_");
                        }
                    } else {
                        params[name] = params[name].concat( storeData.get(i).get(name));
                    }
                }
            }
        }

        var serverResponse = CQ.utils.HTTP.post(this.urlPost, null, params, this);
        return CQ.utils.HTTP.isOk(serverResponse);
    },
    loadContent: function() {

        loadStore = null;

        if (!this.content) {
            var url = CQ.HTTP.externalize(this.urlGet);
            loadStore = new CQ.data.SlingStore({
                url: url+'.infinity.json'
            });
        } else if (this.content instanceof CQ.Ext.data.Store) {
            loadStore = this.content;
        }

        loadStore.load({
            callback: this.processRecords,
            scope: this
        });

    },
    processRecords: function(){

        var tagString = loadStore.getAt(0).get('tags');
        var sourceProxyUrl = firstGrid.getStore().proxy.url;

        if( tagString != undefined && sourceProxyUrl.indexOf( tagString ) == -1 ){
            sourceProxyUrl = sourceProxyUrl + "?tags=" + tagString;
        }
        //alert( "Process Records: " + sourceProxyUrl );
        var updatedSourceStore = new CQ.Ext.data.Store({
            url: sourceProxyUrl,
            reader: reader
        });

        updatedSourceStore.load();
        firstGrid.reconfigure( updatedSourceStore, columnModel );

        var tempHeadlineData = loadStore.getAt(0).get('headline');
        if( tempHeadlineData != undefined ){
            tempHeadlineData = "" + tempHeadlineData;
            var headlineData = new Array();
            headlineData = tempHeadlineData.split('_,');

            var tempPathData = loadStore.getAt(0).get('path');
            var tempDateData = loadStore.getAt(0).get('date');

            var secondGridData = [];

            for( var i = 0; i < headlineData.length; i++ ){
                var headline = headlineData[i].replace('_', '');
                secondGridData.push({headline: headline, path: tempPathData[i], date: tempDateData[i]});
            }

            //destination store
            var destinationStore = new CQ.Ext.data.Store({
                reader: reader
            });

            destinationStore.loadData( secondGridData );

            secondGrid.reconfigure( destinationStore, columnModel );
        }

    }
});
CQ.Ext.reg("gridtogrid", CQ.form.GridtoGrid);

2. Create a component in CQ and in the dialog add a widget node with the following properties.

Screen Shot 2013-10-04 at 12.18.59 PM

  • xtype : gridtogrid –> this matches to what you register the widget as in the javascript.
  • dataSrcUrl –> This is the URL which returns a JSON object that is consumed by the JSONReader in the widget JS
  • jsonReaderConfig –> Configuration for the JSON reader object
    • example: [{name: 'headline', mapping: 'headline'},{name:'date', mapping:'date'}]
  • columnModelConfig –> Configuration for the column model to be used in the left grid.
    • example: [{header: 'Headline', width: 500, sortable: false, dataIndex: 'headline'},{header: 'Date', width: 200, sortable: false, dataIndex: 'date', hidden: true}]
  • sortField –> The name of the field that the data should be sorted on
  • sortDirection –> ASC/DESC
  • name –> name of property on the content node to store the destination list under. It will stored as a String[].

Screen Shot 2013-10-04 at 12.28.30 PM

3. Add the component on your page and double click to open the dialog. You should now see a list on the left and can populate the right based on the capabilities described above (double clicks).

Screen Shot 2013-10-04 at 12.39.13 PM

 

 

4. An example servlet that returns a JSON object to be consumed by the widget above is below

final ValueMap attributes = resource.adaptTo(ValueMap.class);
try{
TidyJSONWriter writer = new TidyJSONWriter(out);
String headline = "Headline Not Set";
Calendar date = null;
String stringDate = null;
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd" );
String path = null;

String tagsParameter = request.getParameter( "tags" );

if( tagsParameter != null ){
String[] tags = tagsParameter.split( "," );

NodeIterator articleList = Headlines.getArticleListByTags( currentNode.getSession(), tags, 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();

//Get Headline
try{
headline = article.getProperty( "headline" ).getValue().getString();
} catch (Exception e){
log.info( "headline doesn't exist, using static value instead" );
}

//Get Date
Node contentNode = currentNode.getSession().getNode( article.getPath().replace( "/contentpar/articleBody", "" ) );
try{
date = contentNode.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = contentNode.getProperty( "jcr:created" ).getValue().getDate();
}

//Convert date to String
if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = article.getPath().replace("/jcr:content/contentpar/articleBody","");
writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();
}
writer.endArray();
}

} else {
NodeIterator articleList = Headlines.getArticleListByModifiedDate( currentNode.getSession(), 25 );

if( articleList != null && articleList.getSize() > 0 ){
writer.array();
for( NodeIterator ni = articleList; articleList.hasNext(); ){
Node article = articleList.nextNode();
Node bodyNode = currentNode.getSession().getNode( article.getPath() + "/contentpar/articleBody" );

//Get Headline
try{
headline = bodyNode.getProperty("headline").getValue().getString();
} catch( Exception e ){
log.info( "headline doesn't exist, using static value instead" );
}

//Get lastModifiedDate
try{
date = article.getProperty( "cq:lastModified" ).getValue().getDate();
} catch (Exception e){
log.info( "cq:lastModified doesn't exist, using jcr:created instead" );
date = article.getProperty( "jcr:created" ).getValue().getDate();
}

if( date != null ){
stringDate = sdf.format( date.getTime() );
} else {
log.info( "Date Object was null" );
}

//Get Path
path = bodyNode.getPath().replace("/jcr:content/contentpar/articleBody","");

writer.object();
writer.key("headline").value( headline );
writer.key("path").value( path );
writer.key("date").value(stringDate);
writer.endObject();

}
writer.endArray();
}
}

} catch (Exception e){
log.info( e.getMessage() );
}

I can post a video of the widget in action if this blog post isn’t descriptive  enough. As always, please post comments/questions and I will try to get back to you as soon as possible.

12:46 PM Permalink
May 13, 2013

CQ 5.6 Out Of The Box Workflow Processes, and what they do/can be used for.

I will try and cover what are all the OOTB workflow processes available in CQ 5.6. The processes in red below are the ones where I will add some explanations to as and when I find time.

  • Collaboration Worklow
    • Approve Comment - This process is to approve a comment posted on the site automatically. It will only approve the comment if you have the “Approve” box checked on the Arguments tab of the wf process dialog.The script used by the process is located under
      • /etc/workflow/scripts/social/set_approved.ecma.
    • Approve Comment Step – This process is to send a notification to a user or group to approve a comment. You have the option to choose the User/Group on the dialog and also an option to send an email.
    • Blog Search Ping – This process submits a blog post to various blog search engines. It uses the BlogSearchPingService for the post. Look at the “Day CQ Social Collaboration Blog Search Engine Ping” class’ configuration in the Felix Console. There are no arguments available for this process.
    • Calendar Subscription – This process can be used to either subscribe or unsubscribe to a calendar. It will either subscribe or unsubscribe the calendar component given as the “target”  property in the workflow metadata to the event or calendar (payload). You can choose whether you want the process to do a subscription or an un-subscription from the process arguments.
    • Check Blog Spam – This process checks a comment for spam. OOTB you can either use Akismet service, you need to enter a valid key and registered URL for the spam service in the configurations for Day CQ Antispam.
    • Check Journal Spam – Same as the Check Blog Process above, uses the the Akismet service.
    • Flush (Replicate) Page - A process to flush(replicate) a page for a given replication agent, the argument should be like “agent:flush” for the dispatcher flush agent. It uses the script “/etc/workflow/scripts/social/flush_page.ecma”.
    • Forum Subscription - Process to subscribe and unsubscribe from a forum.  Subscribes or unsubscribes the given user in the metadata to the payload forum topic. This process is used in conjunction with the User State Toggle Button feature. You can choose the “Subscribe” or “Unsubscribe” argument. Subscribe is the default.
    • Journal Search Ping – Same as the Blog Search Ping process above.
    • QnA Subscription – Same as the Forum/Calendar Subscription process above.
    • Send Email - Sends an email based on an email template. You can either point to a template or put the template in a text area of the Arguments tab of the process. The Day CQ Mail Service must be configured for this to work.
  • DAM Workflow
    • Command Line - A process to execute multiple command line programs (imagemagick for example). Mime type, Commands and Thumbnails are the arguments that can be passed to the process. This is documented pretty well on the process dialog itself.
    • Create Sub Asset - This process will fragment an asset in its subassets. Sub assets are assets stored under another asset, as a result of splitting up the asset in fragments during its import, e.g. the single pages of a multi-page PDF file.
    • Create Thumbnail - This process will create one or more thumbnails for the asset to be processed. This is pretty self explanatory.
    • Create Video Storyboard - A process to create a video storyboard. Workflow process that calls FFMPEG on the command line to create a storyboard of a video. A storyboard consists of several key frames extracted from the video. The key frames will be stored as subassets of the video asset. Also, a merged film strip of the key frames will be stored as a storyboard rendition of the video asset. Frame Count, Start, Maximum Width, Maximum Height , Upscale and Frames are the arguments. Examples: Create 10 frames, starting with the first at 12 seconds into the movie frames:10,start:12 Create 10 frames, each 100 x 80 px, upscaled: frames:10,maxWidth:100,maxHeight:80,upScale:true Create 5 frames, each at a defined position, each 100 x 80 px, upscaled: maxWidth:100,maxHeight:80,[00:05:00],[00:10:00],[00:15:00],[00:20:00],[00:25:00]. Will create thumbnails of size 140×100 and 48×48 with a black letterbox/pillarbox only for assets of the video based mime types.
    • Create Video Thumbnails - Workflow process that calls FFMPEG on the command line to create thumbnails of the image. You can specify the dimension of the thumbnails to be created. For example, using the following workflow step arguments:count:3,index:1,start:10,[140x100],[48x48]. Will create thumbnails of size 140×100 and 48×48 with a black letterbox/pillarbox only for assets having a video-based mime-type.
    • Create Web Enabled Image – This one is pretty well explained on the process dialog itself.
    • Delete Asset – Self Explanatory.
    • Delete DAM Asset - The process will delete the file in the /var/dam when the asset in the /content/dam location got deleted. Deletes an Item for the Payload under the following condition: The Payloads relative path to a given source root exists in a given destination branch. If the Payload points to /content/dam/geometrixx/buildings, the Process checks if an Item exists at /var/dam/geometrixx/buildings. If there is an Item and this Item is not involved in a Workflowm, it will be deleted. Assuming the source and destination arguments were set to match the example.
    • Extract Meta Data – Self Explanatory.
    • Gate Keeper - This process prevents the workflow from being fired if an asset is restored, a version restore for example.
    • IDS Job Process - Workflow process for InDesign assets.
    • Light Box Update Asset - This process sets the entry in the lightbox as new originial to the asset it references. It looks at the “assetRefs” property for the original asset.
    • Media Extraction - InDesign Media Extraction. This process creates a SOAP packet with an embedded EmbeddedScript to be executed on InDesign server. The process arguments lets you choose the ExtendScript library, Extend Scripts and the Links Folder Path.
    • Page Extraction - InDesign Page Extraction. This process step creates a CQ page from an InDesign document.The actual extraction is carried out by a configurable PageExtractionHandler. This step works on asset renditions only. The renditions to use are determined by the PageExtractionHandler as well and are expected to be created in a previous step. Arguments available  are

      extractionHandler: The actual extraction handler class to use for creating the page
      pageName: The name int for the extracted page
      pageRoot: The root path for the extracted page
      pageTemplate: The template to use for the extracted page
      pageDesign: The page design to use for the extracted page
      pageTitle: The page title to use for the extracted page

    • Resize Image - The process dialog for this is pretty self explanatory.
    • Resize Image to Area - Same as above, except that you can enter the area of the image (width x height).
    • Set Last Modified - Self Explanatory
    • Synchronize /var/dam - The SyncContentProcess syncs the content below /var/dam with /content/dam in two different modes (cleanup and sync). Process is only executed if started with a mode argument, the payload exists and is currently not involved in a Workflow.
    • Synchronize Asset - Self Explanatory from the process dialog.
    • Synchronize Content - The SyncContentProcess syncs the content below /content/dam in two selectable modes (cleanup and sync). Expects its Payload to point to a nt:folder. The cleanup mode removes the nodes in /content/dam structure that have no counterpart in the /var structure. The sync mode starts for any nt:file in the branch a Workflow with the Workflow Model Id provided in the arguments and the files path as the payload. The workflow model id argument is the Identifier of a WorkflowModel. This Workflow will be started on Assets added by this Process in mode sync. It is ignored in cleanup mode. Example :/etc/wokflow/models/syncmodell
    • Transcode Video - Self Explanatory.
    • Unarchiver - The process dialog does a very good job of explaining what this process does.
    • XMP Writeback - Writes metadata back to the binary.
  • WCM Workflow - All the OOTB processes in this group are pretty self explanatory.
    • Activate Page/Asset
    • Create Version
    • Deactivate Page/Asset
    • Reverse Replicate Content
  • Workflow
    • AND Split - Puts an AND split in your workflow model. You can choose between 2 and 5 branches.
    • Absolute Time Auto Advancer - Workflow Auto Advance Process.
    • Auto Advancer - Same as above.
    • Call Url - Process to call a URL. Script is located at /etc/workflow/scripts/urlcaller.ecma. Arguments are URL, username and password. Pretty self explanatory once you look at the ECMA script.
    • Container Step - Process to contain a workflow within a workflow. The process dialog lets you choose the workflow model you want to include.
    • Create Task - Process to create a task in the new CQ 5.6 Task Manager. The process dialog does a pretty good job of defining arguments and what they are used for.
    • Delete Node - Self Explanatory.
    • Dialog Participant Step - This process lets you define your custom dialog that will be presented to the user when they want to complete the workflow task from the CQ inbox. This is a good way to have users enter custom data when completing a workflow task.
    • Dynamic Participant Step - This process lets you choose a participant dynamically within the workflow rather than setting it up in the model on creation.
    • Extract Export Data 
    • Form Participant Step - Same as the dialog participant step. Reference here on how to use it.
    • Goto Step - Lets you go back to a particular step in the workflow. At the time of writing this blog, there was a UI bug where if you had the goto step inside a split, the drop down wont show the steps outside the branch of the split. The workaround for this is to manually update the property in CRXDE for the goto step process.
    • Lock Payload Step - Self Explanatory.
    • No Operation - Self Explanatory.
    • OR Split - Self Explanatory.
    • Participant Step - Self Explanatory.
    • Process Assembler
    • Process Step - Self Explanatory.
    • Random Participant Chooser - Self Explanatory.
    • Scene7
    • Sentiments Analyzer
    • Test & Target Offer
    • Unlock Payload Process - Self Explanatory.
    • Watchwords Analyzer
    • Workflow Initiator Participant Chooser - Sample that chooses the workflow initiator as the participant.

As always, please leave comments with questions/concerns and I will try to answer as soon as I can.

11:56 AM Permalink
April 2, 2013

How to create a mobile site in CQ

Recently found some very nice detailed videos on how to create mobile sites in CQ. Thanks for the OP for the videos.

There are 4 videos in the series.

  1. http://www.youtube.com/watch?v=_1Q9MS7MBj0
  2. http://www.youtube.com/watch?v=Qd6zojTpYkk
  3. http://www.youtube.com/watch?v=kt50HicMIn0
  4. http://www.youtube.com/watch?v=lRuT6mDZqcw

Of course the official Mobile page is located here

Enjoy!

4:07 PM Permalink
August 27, 2012

How to prevent users from entering duplicate vanity URL’s

This is something which should come very handy at every CQ implementation. The requirement for vanity URLs is that you cant have two pages in CQ with the same vanity URL. In this blog I will try to go over an implementation which will prevent your content authors from entering duplicate vanity URL in the first place.

1. First thing you will need to do is override the OOTB page component dialog and the tab_basic node. To do this, please copy the following nodes

    • /libs/foundation/components/page/dialog
    • /libs/foundation/components/page/tab_basic

2. Paste these nodes as the child of your projects page component whose “sling:resourceSuperType” is “foundation/components/page”

 

3. For the dialog node, change the path to the basic tab to match the path of the “tab_basic” you pasted in your projects page component.
4. For the “tab_basic” node update the “tab_basic/items/vanity/items/vanityPath/fieldConfig” node to add the following two properties
  • vtype
  • vtypeText
5. Override  the following in your apps folder
  • /libs/cq/ui/widgets/js.txt
  • /libs/cq/ui/widgets/source/widgets
6. To your overridden widgets directory at “/apps/cq/ui/widgets/source/widgets”, add a file called duplicateVanityCheck.js
CQ.Ext.apply(CQ.Ext.form.VTypes, {
duplicateVanityCheck: function(v, f) {var dialog = f.findParentByType("dialog");
var dialogPath = dialog.path;
var cqresponse = CQ.HTTP.get("/apps/duplicateVanityCheck?vanityPath="+v+"&amp;pagePath="+dialogPath);
 
var json = eval(cqresponse);
var vanitypathsjson = json.responseText;
var JSONObj = JSON.parse(vanitypathsjson);
var jsonVanityPath = JSONObj.vanitypaths;
 
if (jsonVanityPath.length == 0) {
return true;
} else {
// check whether the path of the page where the vanity path is defined matches the dialog's path
// which means that the vanity path is legal
return false;
}
 
//alert( "Checking Duplicate" );
}
});

 

7. In your overridden js.txt file, add this line

  • widgets/duplicateVanityCheck.js

 

8. You also need to make sure that your foundation client lib has a dependency on “cq.widgets”.

9. Now, we have to create an OSGi bundle that will query the JCR for an entered vanity and return the JSON that will be used by duplicateVanityCheck.js. The code for this class will look something like below

/**
 * @scr.component immediate="true" metatype="false"
 * @scr.service interface="javax.servlet.Servlet"
 * @scr.property name="sling.servlet.methods" values.0="GET"
 * @scr.property name="sling.servlet.paths" values.1="/apps/duplicateVanityCheck"
 */
 
public class VanityDuplicateCheck extends SlingAllMethodsServlet{
 
    private static final Logger logger = LoggerFactory.getLogger(VanityDuplicateCheck.class);
 
    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
 
        try{
            Session session = request.getResourceResolver().adaptTo(Session.class);
            final String vanityPath = request.getParameter("vanityPath");
            final String pagePath = request.getParameter("pagePath");
            logger.info("vanity path parameter passed is {}", vanityPath);
            logger.info("page path parameter passed is {}", pagePath);
            try {
                QueryManager qm = session.getWorkspace().getQueryManager();
                String xpath = "//element(*)[sling:vanityPath='"+ vanityPath + "']";
                logger.info("xpath is {}", xpath);
 
                Query query = qm.createQuery(xpath, Query.XPATH);
                logger.info("Xpath Query Statement is {}", query.getStatement());
                QueryResult result = query.execute();
                NodeIterator nodes = result.getNodes();
                logger.info("result is ", result.getNodes().toString());
 
                TidyJSONWriter tidyJSONWriter = new TidyJSONWriter(response.getWriter());
 
                tidyJSONWriter.object();
 
                tidyJSONWriter.key("vanitypaths").array();
 
                response.setContentType("text/html");
 
                while (nodes.hasNext()) {
                    Node node = nodes.nextNode();
                    logger.info("Node path is {}", node.getPath());
                    logger.info("Page path is {}", pagePath);
                    if(node != null &amp;&amp; node.getPath().contains("/content"))
                    {
                        // check whether the path of the page where the vanity path is defined matches the dialog's path
                        // which means that the vanity path is legal.
                        if(node.getPath().equals(pagePath))
                        {
                            //do not add that to the list
                            logger.info("Node path is {}", node.getPath());
                            logger.info("Page path is {}", pagePath);
                        } else {
                            tidyJSONWriter.value(node.getPath());
                        }
                    }
                }
 
                tidyJSONWriter.endArray();
                tidyJSONWriter.endObject();
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
            }
            catch(RepositoryException re){
                logger.error( "Error in doGet", re );
            }
        } catch (JSONException e) {
            logger.error( "Error in doGet", e );
        }
    }

8. At this point you should have everything you need to prevent content authors from entering duplicate vanity URL’s. If they do enter a URL that is already in use, it will fail the validation and they will see an error message similar to what you entered in the vtypeText property.

 

Enjoy..and as always, please leave comments/questions and I will try to answer them as soon as possible.

5:49 PM Permalink
June 6, 2012

How to fix CRXDE performance issues

Is CRXDE too slow or is not loading at all? Try these steps below to possibly fix the issue.

Make sure the crxde:paths property doesn’t include anything that you dont want to load in CRXDE.

  • Open the Content Explorer and browse to /etc/crxde/profiles/default, on the right hand panel there will be the crxde:paths property, this defines what nodes CRXDE will try to load. Having nodes like /content will make it slow/unresponsive.

Locate and open the CRXDE.ini file and assign more memory to CRXDE.

  • On OSX right click on CRXDE.app and select Show Package Contents.
  • Browse to Contents/MacOS
  • Open the CRXDE.ini file and change the values for Xms, Xmx and the MaxPermSize to suit your needs and what your system will support.

Delete the .crxde folder

  • CRXDE created a hidden .crxde folder under the users home directory.
  • Delete that folder

Start CRXDE from command line.

  • Open -a CRXDE –args -clean (This is for OSX only)

Hope this helps. Please leave a comment/question and I will try to answer them as soon as I can.

11:26 AM Permalink