Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all 525 articles
Browse latest View live

AEM 6 SP2 - Simple Dispatcher Configuration (Author -> Publish -> Dispatcher)

$
0
0

Goal


Create simple Author -> Publish -> Dispatcher configuration on developer's box (not production). Assuming Author aem is running on port 4502 and Publish aem on port 4503, this post is on adding & configuring dispatcher module on Windows Apache 2.2 Http Server running on port 80. For product documentation on configuring dispatcher check this page and this page


Author - Create Page

1) Start by creating a template on author or download this sample package (contains template Basic Template); installing it creates the following structure (/apps/samples) in CRX (http://localhost:4502/crx/de)





2) Create a page of type Basic Template and add a text component (for information on creating CQ template components check this post). Here is the page created - http://localhost:4502/editor.html/content/experience-aem/english.html





3) The page node structure in author CRX




Publish - View Page

1) Try to view the page on publish, created on author above. As the page isn't published yet, accessing publish http://localhost:4503/content/experience-aem/english.html results in 404

2) Go back to author for publishing the page. Before page is published, package containing the template component needs to be replicated. Access CRXDE package manager of author (http://localhost:4502/crx/packmgr/index.jsp) and replicate the package basictemplate.zip





3) When replication is successful, the necessary /apps/samples node structure should have been created in Publish CRX (http://localhost:4503/crx/de/index.jsp#/apps/samples)

4) Publish the page on author. The node /content/experience-aem/english gets created in publish crx confirming page publish




5) Access the published page on http://localhost:4503/content/experience-aem/english.html




6) User now successfully authored and published  page /content/experience-aem/english.html


Dispatcher - Cache page

1) Using a Web Server and CQ Dispatcher to cache published pages, brings great performance by serving static html and limiting requests sent to publish server for recreating the same pages over and over for each client request

2) Assuming Apache 2.2 Http Server installed, download the dispatcher module. Dispatcher releases are independent of AEM releases, so find the right dispatcher for your OS (Windows), Web Server (Apache) and not AEM - dispatcher-apache2.2-windows-x86-4.1.9.zip (if this dispatcher version was updated, link may not work, download it from the page)

3) Unzip the file dispatcher-apache2.2-windows-x86-4.1.9.zip and copy disp_apache2.2.dll to apache modules folder (eg. C:\dev\code\install\Apache2.2\modules)

4) Open apache httpd.conf file (eg. C:\dev\code\install\Apache2.2\conf\httpd.conf) and add the following line to include dispatcher configuration (added in next steps)

Include conf/dispatcher.conf




4) Create file dispatcher.conf in conf folder (eg. C:\dev\code\install\Apache2.2\conf\dispatcher.conf) and add the following configuration. For more details on available parameters like DocumentRoot check this adobe doc and apache http server documentation

LoadModule dispatcher_module modules\disp_apache2.2.dll

<IfModule disp_apache2.c>
DispatcherConfig conf/dispatcher.any
DispatcherLog logs/dispatcher.log
DispatcherLogLevel 3
DispatcherNoServerHeader 0
DispatcherDeclineRoot 0
DispatcherUseProcessedURL 0
DispatcherPassError 0
</IfModule>

<Directory />
<IfModule disp_apache2.c>
SetHandler dispatcher-handler
ModMimeUsePathInfo On
</IfModule>

Options FollowSymLinks
AllowOverride None
</Directory>

5) #1 loads the dispatcher module, #4 specifies the dispatcher rules file (created in next step)

6) Create file dispatcher.any in conf folder (eg. C:\dev\code\install\Apache2.2\conf\dispatcher.any) with the following configuration

/farms {
/experience-aem {
/clientheaders {
"*"
}
/virtualhosts {
"*"
}
/renders {
/rend01 {
/hostname "127.0.0.1"
/port "4503"
}
}
/filter {
/0001 {
/type "deny" /glob "*"
}
/0002 {
/type "allow" /url "/content*"
}
/0003 {
/type "allow" /url "/etc/designs*"
}
/0004 {
/type "allow" /url "/etc/clientlibs*"
}
}
/cache {
/docroot "C:/dev/code/install/Apache2.2/dispatcher/cache"
/rules {
/0000 {
/glob "*"
/type "allow"
}
}
}
}
}

7) #2 is name of farm which could be any, #10 render is the publish instance serving pages to be cached (1:1 dispatcher to publish mapping recommended). #17 first denies access to every resource and making exceptions at #20 to allow resources under /content, #23 for allowing site css, #26 for js files, minimally needed for typical websites. #30 specifies the cache folder where static content generated from publish servers is cached for any future requests until any cached content is invalidated (next sections). Rule at #32 directs dispatcher to cache all content. For more indepth dispatcher configuration check adobe documentation

8) With the above configuration in place, restart apache server, open a new browser instance and access the sample page on dispatcher running on default port 80 - http://localhost/content/experience-aem/english.html




9) The following statements should haven be logged in logs/dispatcher.log (eg. C:\dev\code\install\Apache2.2\logs\dispatcher.log) and cached content available in dispatcher/cache folder (eg. C:\dev\code\install\Apache2.2\dispatcher\cache)






Dispatcher - why is content not cached?

1) If the page requested from dispatcher (http://localhost/content/experience-aem/english.html) returns a 200 and available, but not cached in dispatcher; the reason could be login-token cookie present in browser. So you are logged into AEM in one tab and try to access the page on dispatcher in another tab; an AEM session is available and the following statement is logged in dispatcher.log



2) The statement above request contains authorization is logged when there is a login-token cookie sent with request (in this configuration, author AEM and dispatcher both have domain localhost). Using any REST client (chrome extension POSTMAN) to view the request headers and cookies...





3) Try accessing http://localhost/content/experience-aem/english.html by logging out of AEM or opening a new browser instance or deleting the login-token cookie from browser or configure /allowAuthorized in dispatcher.any (for more information on caching requests containing authentication information check this adobe documentation) and the page html should be cached

           Request sent with no login-token cookie (cookie deleted from browser)



           Log statement for create cache



           Cached file on file system


   

Dispatcher - Cache Invalidation 

1) Invalidating dispatcher cache is required to make sure it doesn't serve stale content. Auto invalidation or sending cache invalidate requests - /dispatcher/invalidate.cache are ways to delete  (invalidate) dispatcher cache

2) For publish instance to send dispatcher invalidate cache requests when there is content update on a page, the Dispatcher Flush Agent needs to be configured

3) Access http://localhost:4503/etc/replication/agents.publish.html, click on Dispatcher Flush (flush) agent -> Settings Edit and enable it (if you are not logged into publish instance, a 404 is returned; login to publish CRX http://localhost:4503/crx/de)




4) Enter the invalidate cache url of dispatcher running on port 80 - http://localhost:80/dispatcher/invalidate.cache




5) Here is the dispatch flush agent on publish enabled




6) Publish is now configured to send invalidate cache requests to dispatcher; lets modify the page on author and publish it




7) When the page gets replicated (published) from author to publish the following statements should be logged in publish\crx-quickstart\logs\error.log confirming the page flush from dispatcher cache




8) Dispatcher flush request received and logged, in dispatcher.log as shown in picture below. The page should have been deleted from file system (eg. from C:\dev\code\install\Apache2.2\dispatcher\cache\content\experience-aem)




9) Access the page again to see updated content (and cache it)


10) A manual cache invalidation request can also be sent using any rest client (or curl) by adding the following custom headers. To send a cache invalidation request on page /content/experience-aem/english.html, add the following headers to request url http://localhost:80/dispatcher/invalidate.cache

                CQ-Action: Activate
                CQ-Handle: /content/experience-aem/english
                CQ-Path: /content/experience-aem/english




11) Dispatcher should be protected from unauthorized cache invalidate requests (hackers). On production systems /allowedClients should be configured in dipsatcher.any with the publish server ips sending cache invalidate requests, check adobe documentation




AEM 6 SP2 - Classic UI Side By Side Version Diff Compare

$
0
0

Goal


Create a mirror view or side by side compare of page version diff and current version so that user can spot changes and view current version at the same time in single window

The demo uses iframes scaled down do 80%, to show version diff and current version in two panes with pages loaded as wcmmode disabled; user is not logged into aem in the iframes, so page shows anonymous view

To keep view in sync, the two iframes listen to scroll events of each other; if view sync is not required, comment evenScroll() calls

When the content finder is open, diff extension window some times doesn't show on top of side kick and content finder (at the front) as shown here, a known bug

Demo | Package Install


Side by Side Diff button





Side by Side Compare window





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classic-ui-side-by-side-diff

2) Create node /apps/classic-ui-side-by-side-diff/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets and dependencies of type String[] with value underscore

3) Create file (nt:file) /apps/classic-ui-side-by-side-diff/clientlib/js.txt and add

                       side-by-side-diff.js

4) Add the following code in /apps/classic-ui-side-by-side-diff/clientlib/side-by-side-diff.js


CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.SideDiff = {
LEFT_PANE: 'eaem-left-pane',
RIGHT_PANE: 'eaem-right-pane',
BUT_ID: 'eaem-side-diff',

createDiffWindow: function(lUrl, rUrl){
var E = ExperienceAEM.SideDiff;

//scale down the iframe to 80% for better view
var frameStyle= "style='width: 1000px; height: 1000px; " +
"-webkit-transform: scale(0.8); -webkit-transform-origin: 0 0; " +
"-moz-transform: scale(0.8); -moz-transform-origin: 0px 0px;'";

var lFrame = "<iframe " + frameStyle + " id='" + E.LEFT_PANE + "' src='" + lUrl + "'></iframe>";
var rFrame = "<iframe " + frameStyle + " id='" + E.RIGHT_PANE + "' src='" + rUrl + "'></iframe>";

var divStyle = "width: 800px; height: 1000px; ";

var lBox = new CQ.Ext.BoxComponent({
autoEl: { tag: 'div', html: lFrame, style: divStyle }
});

var rBox = new CQ.Ext.BoxComponent({
autoEl: { tag: 'div', html: rFrame, style: divStyle }
});

var panel = new CQ.Ext.Panel({
header: false,
border: false,
layout:'fit',
height: 820,
width: 1600,
items:[{
layout:'table',
layoutConfig: {
columns: 2
},
items:[lBox , rBox ]
}]
});

var config = {
title: "Side by Side Compare",
x: 25,
y:37,
items: [ panel ]
};

var win = new CQ.Ext.Window(config);

win.on('show', function(){
var $left = $("#" + E.LEFT_PANE), $right = $("#" + E.RIGHT_PANE);

//to keep view in sync, the two iframes listen to scroll events of each other
evenScroll($left, $right);
evenScroll($right, $left);

win.toFront(true);
});

function evenScroll($one, $two){
$one.load(function(){
$($one[0].contentWindow.document).scroll(function(){
var $this = $(this);
$two[0].contentWindow.scrollTo($this.scrollLeft(),$this.scrollTop());
});
});
}

win.show();
},

addButton: function(){
var E = ExperienceAEM.SideDiff;

//buttons panel is the first component of "Restore Version" panel
var bPanel = this.getComponent(0);
var sideDiff = bPanel.getComponent(E.BUT_ID);

//check if side diff button was already added
if( sideDiff != null){
return;
}

var sk = CQ.WCM.getSidekick();

//get the grid containing versions.. grid panel is the second component of "Restore Version" panel
//grid is the only component in grid panel
var grid = this.getComponent(1).getComponent(0);

sideDiff = {
xtype: "button",
id: E.BUT_ID,
text: "| Diff |",
handler: function() {
var rec = grid.getSelectionModel().getSelected();

if (!rec) {
CQ.Ext.Msg.alert("Select", "Please select a version");
return;
}

var HTTP = CQ.HTTP;

var left = HTTP.externalize(sk.getPath() + ".html");
var right = HTTP.externalize(sk.getPath() + ".html");

left = HTTP.addParameter(left, "cq_diffTo", rec.data.label);
left = HTTP.addParameter(left, "wcmmode", "disabled");
left = HTTP.noCaching(left);

right = HTTP.addParameter(right, "wcmmode", "disabled");
right = HTTP.noCaching(right);

E.createDiffWindow(left, right);
}
};

bPanel.add(sideDiff);

this.doLayout();
},

getPanel: function(){
var sk = CQ.WCM.getSidekick();

if(!sk){
return null;
}

var rVersion = null;

try{
var vPanel = sk.panels[CQ.wcm.Sidekick.VERSIONING];

if(!vPanel){
return null;
}

rVersion = vPanel.findBy(function(comp){
return comp["title"] == "Restore Version";
});

if(_.isEmpty(rVersion)){
return null;
}
}catch(err){
console.log("Error adding side by side diff", err);
}

return rVersion[0];
}
};

(function(){
var pathName = window.location.pathname;

if( ( pathName !== "/cf" ) && ( pathName.indexOf("/content") !== 0)){
return;
}

var E = ExperienceAEM.SideDiff;

var SK_INTERVAL = setInterval(function(){
//get the Restore Version panel
var panel = E.getPanel();

if(!panel){
return;
}

clearInterval(SK_INTERVAL);

panel.on('activate', E.addButton);
}, 250);
})();




AEM 6 SP2 - Show Confirm Box before Node Move in CRXDE Lite

$
0
0
This is an unconventional way of extending CRXDE Lite; for desperate situations only

Goal


This post is on showing confirm prompt when user attempts node move in CRXDE Lite (http://localhost:4502/crx/de) or use shift key + move for moving a node

Ext.Msg.confirm works asynchronously, so if a confirm box is absolutely needed use browser confirm

Demo | Package Install


Confirm Prompt





No Shift Press while Drag





Solution


Follow the two steps below to extend CRXDE Lite and add necessary JS to show confirm. First step is Not Upgrade-Proof, so when CQ is upgraded, the first step may have to be repeated

Step 1 - Update CRXDE Lite Jar

All we do in this step is copy (back it up just in case if something goes wrong) the serialized CRXDE lite jar, open it and add a small chunk of JS code so that any extensions we code are loaded by the added JS logic when lite is opened in browser.

1) Access bundles console http://localhost:4502/system/console/bundles and find the CRXDE Support bundle





2) Search for the serialized bundle on filesystem and copy it to a temp location (take a backup before modifying). On my AEM 6 SP2 its available in author\crx-quickstart\launchpad\installer (rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser)

3) Rename the copied .ser file to .jar (eg. rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar-1415034571045.ser -> rsrc-com.adobe.granite.crxde-lite-1.0.66-CQ600-B0001.jar)

4) Open the jar using zip executable (say winrar), open file docroot\js\start.js in any text editor and add following code at the end. Save file and a winrar confirmation should popup asking if the jar should be updated with saved file.

Ext.onReady(function() {
var loadLiteExtns = function(){
Ext.Ajax.request({
url: "/apps/ext-crxde-lite/files.txt",
success: function(response, options) {
var js = response.responseText;

if(!js){
return;
}

js = js.split("\n");

Ext.each(js, function(jsPath) {
Ext.Ajax.request({
url: jsPath,
success: function(response, options) {
eval(response.responseText);
}
});
});
}
});
};

loadLiteExtns();
});


5) In the above steps we added necessary code to load the extension files entered in /apps/ext-crxde-lite/files.txt. So whenever a new CRXDE Lite extension is needed a new line with extension file path can be added in /apps/ext-crxde-lite/files.txt

6) Access http://localhost:4502/system/console/bundles, click Install/Update... to upload and update CQ with the new CRXDE Support jar having necessary code to load the CRXDE Lite extension files.

Step 2 - Add extension files in CRX

In this step we add the JS file containing logic to show confirm

1) Access http://localhost:4502/crx/de

2) Create node /apps/ext-crxde-lite of type nt:folder

3) Create node /apps/ext-crxde-lite/files.txt of type nt:file and add the following line. The logic added in Step 1 reads this file for loading JS extension files added as paths

                                 /apps/crxde-move-node-confirm/move-confirm.js

4) Create node /apps/crxde-move-node-confirm/move-confirm.js of type nt:file and add the following code

Ext.onReady(function(){
var INTERVAL = setInterval(function(){
var tree = Ext.getCmp(CRX.ide.TREE_ID);

if(!tree){
return;
}

clearInterval(INTERVAL);

var listeners = tree.initialConfig.listeners;

tree.removeListener("beforenodedrop", listeners.beforenodedrop, tree);

tree.on("beforenodedrop", function(dropEvent){
/*var r = confirm("You are trying to move a node, Are you sure?");

if (r) {
return listeners.beforenodedrop.call(tree, dropEvent);
} else {
return false;
}*/

//uncomment this block for stopping node move, with no shift key press
var shiftKey = dropEvent.rawEvent.browserEvent.shiftKey;

if(!shiftKey){
Ext.Msg.alert("Alert", "If you'd like to move a node, press shift on keyboard before dragging");
return false;
}

return listeners.beforenodedrop.call(tree, dropEvent);
});
}, 250);
});



AEM 6 SP 2- Classic UI Create Page and Open in Scaffolding View

$
0
0

Goal


Extend the Classic UI create page dialog to create & open in scaffolding view. A similar post on extending the dialog to provide create & viewis here

Demo | Package Install





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/create-page-open-scaffolding-view

2) Create clientlib (type cq:ClientLibraryFolder/apps/create-page-open-scaffolding-view/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/create-page-open-scaffolding-view/clientlib/js.txt, add the following

                         open.js

4) Create file ( type nt:file ) /apps/create-page-open-scaffolding-view/clientlib/open.js, add the following code

(function(){
var cqCreatePageDialog = CQ.wcm.Page.getCreatePageDialog;

CQ.wcm.Page.getCreatePageDialog = function(parentPath){
var dialog = cqCreatePageDialog(parentPath);

var panel = dialog.findBy(function(comp){
return comp["jcr:primaryType"] == "cq:Panel";
}, dialog);

if(!panel || panel.length == 0){
return;
}

dialog.buttons.splice(0,0,new CQ.Ext.Button( {
text: "Scaffolding View",
width: 140,
tooltip: 'Create page and open in scaffolding view',
handler: function(button){
dialog.ok(button, function(form, resp){
try{
var text = resp.response.responseText;
var loc = text.substring(text.indexOf("\"", text.indexOf("href=")) + 1);

loc = "/cf#" + loc.substr(0, loc.indexOf("\"")) + ".scaffolding.html";
window.location = loc;
}catch(err){
console.log("page create and scaffolding view - error parsing html response");
}
});
}}
));

return dialog;
}
})();

AEM 6 SP2 - Change Parsys Border and Text Color in Classic UI

$
0
0

Goal


Change the color of parsys. In Classic UI, the parsys border color is #d3ea9a; if your website background happens to be of same color or site CSS cannot contrast CQ parsys, the following simple css override might help

Package Install


Product Parsys





Extension Parsys





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-change-parsys-color

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-change-parsys-color/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-change-parsys-color/clientlib/css.txt, add the following

                         highlight.css

4) Create file ( type nt:file ) /apps/classic-ui-change-parsys-color/clientlib/highlight.css, add the following styles

#CQ .cq-editrollover-highlight-left {
background-color: red !important;
}

#CQ .cq-editrollover-highlight-bottom {
background-color: red !important;
}

#CQ .cq-editrollover-highlight-top {
background-color: red !important;
}

#CQ .cq-editrollover-highlight-right {
background-color: red !important;
}

#CQ .cq-editrollover-insert-message {
color: red !important;
}


AEM 6 SP2 - Sample Datasource Touch UI Select Listener

$
0
0

Goal


Add two Granite Select widgets - /libs/granite/ui/components/foundation/form/select with listeners, each listening to the select event of other. In this sample, Country Select widget and Capital Select widget are fed with data sources, countries and capitals respectively for options, selecting country changes capital, vice versa

Demo | Package Install





Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-select-listener/clientlib, set a property categories of String type to cq.authoring.dialog and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touchui-select-listener/clientlib/js.txt, add the following

                         listener.js

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

(function ($, $document) {
"use strict";

var COUNTRY = "./country", CAPITAL = "./capital";

function adjustLayoutHeight(){
//with only two selects, the second select drop down is not visible when expanded, so adjust the layout height
//fixedcolumns i guess doesn't support property height, so fallback to jquery
//http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/layouts/fixedcolumns/index.html
$(".coral-FixedColumn-column").css("height", "18rem");
}

$document.on("dialog-ready", function() {
adjustLayoutHeight();

//get the country widget
var country = new CUI.Select({
element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
});

//get the capital widget
var capital = new CUI.Select({
element: $("[name='" + CAPITAL +"']").closest(".coral-Select")
});

if(_.isEmpty(country) || _.isEmpty(capital)){
return;
}

//workaround to remove the options getting added twice, using CUI.Select()
country._selectList.children().not("[role='option']").remove();
capital._selectList.children().not("[role='option']").remove();

//listener on country select
country._selectList.on('selected.select', function(event){
//select country's capital and throw change event for touchui to update ui
capital._select.val(event.selectedValue).trigger('change');
});

//listener on capital select
capital._selectList.on('selected.select', function(event){
//select capital's country and throw change event for touchui to update ui
country._select.val(event.selectedValue).trigger('change');
});
});
})($, $(document));


5) Create a simple datasource for countries - /apps/touchui-select-listener/datasource/country/country.jsp

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

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
final Map<String, String> countries = new LinkedHashMap<String, String>();

countries.put("INDIA", "India");
countries.put("USA", "United States");
countries.put("CHINA", "China");

final ResourceResolver resolver = resourceResolver;

DataSource ds = new SimpleDataSource(new TransformIterator(countries.keySet().iterator(), new Transformer() {
public Object transform(Object o) {
String country = (String) o;
ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

vm.put("value", country);
vm.put("text", countries.get(country));

return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
}
}));

request.setAttribute(DataSource.class.getName(), ds);
%>


6) Create datasource for capitals - /apps/touchui-select-listener/datasource/capital/capital.jsp

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

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
final Map<String, String> capitals = new LinkedHashMap<String, String>();

capitals.put("INDIA", "New Delhi");
capitals.put("USA", "Washington DC");
capitals.put("CHINA", "Beijing");

final ResourceResolver resolver = resourceResolver;

DataSource ds = new SimpleDataSource(new TransformIterator(capitals.keySet().iterator(), new Transformer() {
public Object transform(Object o) {
String capital = (String) o;
ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

vm.put("value", capital);
vm.put("text", capitals.get(capital));

return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
}
}));

request.setAttribute(DataSource.class.getName(), ds);
%>


7) The Touch UI dialog xml - /apps/touchui-select-listener/sample-select-listener/cq:dialog

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Multifield TouchUI Component"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="en/cq/current/wcm/default_components.html#Text">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<fieldset
jcr:primaryType="nt:unstructured"
jcr:title="Sample Select"
sling:resourceType="granite/ui/components/foundation/form/fieldset">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<country
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/select"
fieldLabel="Country"
name="./country">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/touchui-select-listener/datasource/country"
addNone="{Boolean}true"/>
</country>
<capital
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/select"
disabled="{Boolean}false"
fieldLabel="Capital"
name="./capital">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/touchui-select-listener/datasource/capital"
addNone="{Boolean}true"/>
</capital>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</content>
</jcr:root>


8) The component structure in CRXDE Lite






AEM 6 SP2 - TouchUI Adding Dynamic Select Options

$
0
0

Goal


Add two Granite Select widgets - /libs/granite/ui/components/foundation/form/select with second select being dynamic. In this example, selecting language fills the country select widget with available countries for that language in CRX /libs/wcm/core/resources/languages

Demo | Package Install





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-fill-second-select

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-fill-second-select/clientlib, set a property categories of String type to cq.authoring.dialog and dependencies of type String[] to underscore

3) Create file ( type nt:file ) /apps/touchui-fill-second-select/clientlib/js.txt, add the following

                         listener.js

4) Create file ( type nt:file ) /apps/touchui-fill-second-select/clientlib/listener.js, add the following code

(function ($, $document) {
"use strict";

var LANGUAGE = "./language", COUNTRY = "./country";

function adjustLayoutHeight(){
//with only two selects, the second select drop down is not visible when expanded, so adjust the layout height
//fixedcolumns i guess doesn't support css property height, so fallback to jquery
//http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/layouts/fixedcolumns/index.html
$(".coral-FixedColumn-column").css("height", "20rem");
}

$document.on("dialog-ready", function() {
adjustLayoutHeight();

//http://docs.adobe.com/docs/en/aem/6-0/develop/ref/granite-ui/api/jcr_root/libs/granite/ui/components/foundation/form/select/index.html
var language = new CUI.Select({
element: $("[name='" + LANGUAGE +"']").closest(".coral-Select")
});

var country = new CUI.Select({
element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
});

if(_.isEmpty(country) || _.isEmpty(language)){
return;
}

var langCountries = {};

//workaround to remove the options getting added twice, using CUI.Select()
language._selectList.children().not("[role='option']").remove();

function fillCountries(selectedLang, selectedCountry){
country._select.children().remove();
country._selectList.children().remove();

_.each(langCountries, function(value, lang){
if( (lang.indexOf(selectedLang) !== 0) || (value.country == "*") ){
return;
}

$("<option>").appendTo(country._select)
.val(lang).html(value.country);
});

country = new CUI.Select({
element: $("[name='" + COUNTRY +"']").closest(".coral-Select")
});

if(!_.isEmpty(selectedCountry)){
country._select.val(selectedCountry).trigger('change');
}
}

//listener on language select for dynamically filling the countries on language select
language._selectList.on('selected.select', function(event){
fillCountries(event.selectedValue);
});

//get the langs list
$.getJSON("/libs/wcm/core/resources/languages.2.json").done(function(data){
langCountries = data;

var $form = country.$element.closest("form");

//get the second select box (country) saved value
$.getJSON($form.attr("action") + ".json").done(function(data){
if(_.isEmpty(data)){
return;
}

fillCountries(language.getValue(), data.country);
})
});
});
})($, $(document));

5) Create a simple datasource for languages - /apps/touchui-fill-second-select/datasource/language/language.jsp


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

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.LinkedHashMap" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>

<%
final Map<String, String> languages = new LinkedHashMap<String, String>();

languages.put("ar", "Arabic");
languages.put("en", "English");
languages.put("de", "German");

final ResourceResolver resolver = resourceResolver;

DataSource ds = new SimpleDataSource(new TransformIterator(languages.keySet().iterator(), new Transformer() {
public Object transform(Object o) {
String language = (String) o;
ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>());

vm.put("value", language);
vm.put("text", languages.get(language));

return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
}
}));

request.setAttribute(DataSource.class.getName(), ds);
%>

7) The Touch UI dialog xml - /apps/touchui-fill-second-select/fill-second-select/cq:dialog

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Select TouchUI Component"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="en/cq/current/wcm/default_components.html#Text">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<fieldset
jcr:primaryType="nt:unstructured"
jcr:title="Sample Select"
sling:resourceType="granite/ui/components/foundation/form/fieldset">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<language
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/select"
fieldLabel="Language"
name="./language">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/touchui-fill-second-select/datasource/language"
addNone="{Boolean}true"/>
</language>
<country
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/select"
fieldLabel="Country"
name="./country"/>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</content>
</jcr:root>

8) The component structure in CRXDE Lite

AEM 6 SP2 - Date Time Auto Advancer

$
0
0

Goal


Create a Date Time Auto Advancer extending ootb Absolute Time Auto Advancer to provide the times of day or exact date at which the participant step using this handler should time out.

Demo | Package Install | Source Code


The following screen grab shows Request for Activation workflow's Waiting for activation step using apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer





Selecting Date Time for Timeout shows a dialog with multifields for setting the Times in a day and exact Date & Times




In the above example, for workflow Request for Activation, Waiting for Activation step times out at the earliest time available after step Approve Content is completed. So lets say Approve Content was completed for an asset at 6:30 PM, the handler times out  at 10:30 PM and flow moves to next step Activate Content; asset gets activated at may be 10:31 PM, depending on how big the replication queue is....


Timeout values added in multifield, set in CRX





A sample sling job scheduled for Waiting for Activation





Solution


1) Create a OSGI service apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer extending com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer. This is the handler selected for handling date time timeouts. If no time or date is selected for timeout, handler falls back to its super AbsoluteTimeAutoAdvancer for handling timeouts

package apps.experienceaem.autoadvancer.datetime;

import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.job.AbsoluteTimeoutHandler;
import com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.util.*;

@Component(metatype = false)
@Service(value={WorkflowProcess.class, AbsoluteTimeoutHandler.class})
@Properties({
@Property(name="service.description", value="Experience AEM Date Time Auto Advancer Process"),
@Property(name="process.label", value = "Experience AEM Date Time Auto Advancer")
})
public class DateTimeAutoAdvancer extends AbsoluteTimeAutoAdvancer {
protected final Logger log = LoggerFactory.getLogger(DateTimeAutoAdvancer.class);

private static SimpleDateFormat JS_JAVA_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
private static String DATE_TIMES_PROP = "eaemTimeoutDateTimes";

static {
JS_JAVA_FORMATTER.setTimeZone(TimeZone.getTimeZone("GMT"));
}

private void addTimes(Date currentDate, JSONArray times, List<Long> dtNumbers) throws Exception{
if( (times == null) || (times.length() == 0)){
return;
}

Calendar cal;

String time[], hhmm[], ampm;
int hour, min, nextDayHour = -1, nextDayMin = -1;

for(int i = 0, len = times.length(); i < len; i++){
time = ((String)times.get(i)).split("");

hhmm = time[0].split(":");
ampm = time[1];

cal = Calendar.getInstance();

hour = NumberUtils.createInteger(hhmm[0]);
hour = (ampm.equalsIgnoreCase("AM")) ? hour : hour + 12;
min = NumberUtils.createInteger(hhmm[1]);

cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, min);

//get the earliest time of next day, if a page/asset was approved after the timeout times set
//in a day, are passed
if(i == 0){
nextDayHour = hour;
nextDayMin = min;
}else{
if(hour < nextDayHour){
nextDayHour = hour;
nextDayMin = min;
}else if(hour == nextDayHour){
if( min < nextDayMin ){
nextDayMin = min;
}
}
}

//skip past dates
if(currentDate.getTime() > cal.getTimeInMillis()){
continue;
}

dtNumbers.add(cal.getTimeInMillis());
}

if(dtNumbers.isEmpty()){
cal = Calendar.getInstance();

cal.add(Calendar.DATE, 1);
cal.set(Calendar.HOUR_OF_DAY, nextDayHour);
cal.set(Calendar.MINUTE, nextDayMin);

dtNumbers.add(cal.getTimeInMillis());
}
}

private void addDateTimes(Date currentDate, JSONArray dateTimes, List<Long> dtNumbers)
throws Exception{
if( (dateTimes == null) || (dateTimes.length() == 0)){
return;
}

String dateStr = null; Date date = null;

for(int i = 0, len = dateTimes.length(); i < len; i++){
dateStr = (String)dateTimes.get(i);

date = JS_JAVA_FORMATTER.parse(dateStr);

//skip past dates
if(currentDate.getTime() > date.getTime()){
continue;
}

dtNumbers.add(date.getTime());
}
}

public long getTimeoutDate(WorkItem workItem) {
List<Long> dtNumbers = new ArrayList<Long>();
Date currentDate = new Date();

try{
String dateTimesVal = workItem.getNode().getMetaDataMap().get(DATE_TIMES_PROP, String.class);

if(StringUtils.isEmpty(dateTimesVal)){
return super.getTimeoutDate(workItem);
}

JSONObject dt = new JSONObject(dateTimesVal);

JSONArray times = (JSONArray)dt.get("times");
JSONArray dateTimes = (JSONArray)dt.get("datetimes");

addTimes(currentDate, times, dtNumbers);
addDateTimes(currentDate, dateTimes, dtNumbers);

Collections.sort(dtNumbers);
}catch(Exception e){
log.error("Could not calculate timeout", e);
}

// get the most recent date&time in future, at which the auto advancer should timeout
return dtNumbers.isEmpty() ? super.getTimeoutDate(workItem) : dtNumbers.get(0);
}
}


2) If any time of day or exact date, is in the past, handler simply ignores it. Handler adds all the next timeout dates in a collection, sorts them and gets the lowest long which is the most recent time in future the handler should time out

3) #62, #86, if times are set in dialog, but time of day is already in the past (say user has approved page/asset very late in the night) and no more timeouts are available for the day, handler tries to find earliest time of next day, it should timeout

4) Install the OSGI service Experience AEM Date Time Auto Advancer in CRX folder /apps/classicui-auto-advancer-date-time-selector

5) Login to CRXDE Lite, create clientlib (type cq:ClientLibraryFolder/apps/classicui-auto-advancer-date-time-selector/clientlib and set a property categories of String type to cq.widgets and dependencies type String[] to underscore

6) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/js.txt, add the following

                         add-date-time.js

7) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/add-date-time.js, add the following code

(function(){
var DATE_TIME_AA_CLASS = "apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer";
var DATE_TIME_SEL_VALUE = -1;
var DATE_TIMES_PROP = "./metaData/eaemTimeoutDateTimes";

var pathName = window.location.pathname;

if( pathName.indexOf("/etc/workflow") != 0 ){
return;
}

function getDialog(dateTimesHidden){
var datetimes = new CQ.form.MultiField({
border: false,
fieldLabel: "Date and Time",
fieldConfig: {
"xtype": "datetime"
}
});

var times = new CQ.form.MultiField({
border: false,
fieldLabel: "Time",
fieldConfig: {
"xtype": "timefield"
}
});

var text = "Time: The times in a day, handler should timeout - Date and Time: The exact date&time, handler should timeout" +
"Example: Workflow - Request for Activation, Handler Step: Waiting for Activation, Previous Step - Approve Content" +
"Scenario 1: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and 'Approve Content' step is completed at '4:30 PM', " +
"'Waiting for Activation' step is guaranteed to timeout at '5:00 PM'" +
"Scenario 2: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and " +
"'Date and Time' is set to 'May 04, 2015, 5:10 PM', 'June 13, 2015, 4:30 PM'; " +
"If 'Approve Content' step completes on 'May 04, 2015, 4:20 PM', the 'Waiting for Activation' timesout at " +
"'5:00 PM' and not '5:10 PM'. 'Approve Content' step completed on 'June 13, 2015, 4:20 PM' will timeout " +
"'Waiting for Activation' at '4:30 PM'";

var config = {
"jcr:primaryType": "cq:Dialog",
width: 600,
height: 400,
title: "Date Time",
items: {
"jcr:primaryType": "cq:Panel",
bodyStyle: "padding: 10px",
html: text,
items: {
"jcr:primaryType": "cq:WidgetCollection",
times: times,
datetimes: datetimes
}
},
ok: function(){
var value = {
times: times.getValue(),
datetimes: datetimes.getValue()
};

dateTimesHidden.setValue(JSON.stringify(value));

this.close();
},
cancel: function(){
this.close();
}
};

var dateTimeValues = dateTimesHidden.getValue();

if(!_.isEmpty(dateTimeValues)){
dateTimeValues = JSON.parse(dateTimeValues);

datetimes.setValue(dateTimeValues.datetimes);
times.setValue(dateTimeValues.times);
}

return CQ.WCM.getDialog(config);
}

function initTimeoutSelection(dialog, timeoutType){
var dateTimeOption = {
text: "Date Time",
value: DATE_TIME_SEL_VALUE
};

var dateTimesHidden = dialog.addHidden({ "./metaData/eaemTimeoutDateTimes" : ""})[DATE_TIMES_PROP];

$.getJSON(dialog.form.url + ".infinity.json").done(function(data){
if(_.isEmpty(data.metaData) || _.isEmpty(data.metaData.eaemTimeoutDateTimes)){
return;
}
dateTimesHidden.setValue(data.metaData.eaemTimeoutDateTimes);
});

timeoutType.options.push(dateTimeOption);

timeoutType.setOptions(timeoutType.options);

timeoutType.reset();

return dateTimesHidden;
}

function handleTimeoutChange(timesDialog, handlerType, dateTimesHidden, timeoutValue){
if(timeoutValue != DATE_TIME_SEL_VALUE){
return timesDialog;
}

if(handlerType.getValue() != DATE_TIME_AA_CLASS){
CQ.Ext.Msg.alert("Invalid", "Handler selected cannot handle datetime");

this.setValue("Off");

return timesDialog;
}

//to handle the change event fired by combo in timeout selection
if(timesDialog && timesDialog.isVisible()){
return timesDialog;
}

timesDialog = getDialog(dateTimesHidden);

timesDialog.show();

return timesDialog;
}

function handleTimeoutHandlerChange(timeoutType, handlerValue){
if(handlerValue != DATE_TIME_AA_CLASS){
timeoutType.setValue("Off");
}
}

function registerDateTime(dialog, handlerType, timeoutType){
if(dialog.eaemListenersAdded){
return;
}

dialog.eaemListenersAdded = true;

var dateTimesHidden = initTimeoutSelection(dialog, timeoutType);

var timesDialog;

handlerType.on("selectionchanged", function(t, value){
handleTimeoutHandlerChange(timeoutType, value);
});

timeoutType.on("selectionchanged", function(t, value){
timesDialog = handleTimeoutChange.call(this, timesDialog, handlerType, dateTimesHidden, value);
});
}

function findTimeoutHandler(editable){
function handler(){
var dialog = editable.dialogs[CQ.wcm.EditBase.EDIT];

var selTypes = dialog.findByType("selection");

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

var handlerType, timeoutType;

_.each(selTypes, function(selType){
if(selType.name == "./metaData/timeoutHandler"){
handlerType = selType;
}else if(selType.name == "./metaData/timeoutMillis"){
timeoutType = selType;
}
});

//wait until the dialog gets opened by ootb handlers & initialized
if(!handlerType || !timeoutType){
return;
}

clearInterval(INTERVAL);

registerDateTime(dialog, handlerType, timeoutType);
}

var INTERVAL = setInterval(handler, 250);
}

var INTERVAL = setInterval(function(){
var editables = CQ.WCM.getEditables();

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

clearInterval(INTERVAL);

_.each(editables, function(editable){
editable.el.on('dblclick', function(e){
findTimeoutHandler(editable);
}, this);
});
}, 250);
})();


8) The JS extension waits until Handler and Timeout select widgets are available, adds the Date Time option and multifields dialog

9) Extension structure in CRXDE Lite






AEM 6 SP2 - Classic UI Show Html5 Smart Image Mouse Coordinates

$
0
0

Goal


Show the (X,Y) coordinates of Mouse on Html5 Smart Image (CQ.html5.form.SmartImage) added in Image Component. Mousemove on the image to add coordinates in a textfield added to map tool. Note, this is plain vanilla, may not work as expected if image is processed using crop, zoom, rotate etc.

Demo | Package Install





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-smart-image-coordinates

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-smart-image-coordinates/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-smart-image-coordinates/clientlib/js.txt, add the following

                         coords.js

4) Create file ( type nt:file ) /apps/classic-ui-smart-image-coordinates/clientlib/coords.js, add the following code

(function(){
if (typeof window.ExperienceAEM == "undefined") {
window.ExperienceAEM = {};
}

ExperienceAEM.showMouseCoordinates = function(image){
if(!image || !image.imagePanel || !image.imageToolDefs){
return;
}

var imgTools = image.imageToolDefs,
mapTool, imageOffsets = image.imagePanel.imageOffsets;

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

var mapCoords = mapTool.userInterface.findBy(function(comp){
return comp["itemId"] == "areaDefCoords";
})[0];

var coords = new CQ.Ext.form.TextField({
fieldLabel: "Mouse"
});

mapCoords.ownerCt.add(coords);
mapCoords.ownerCt.doLayout();

var $img = $(image.imagePanel.el.dom).find("img");

$img.mousemove(function(event) {
var offset = $(this).offset(),
relX = (event.pageX - offset.left),
relY = (event.pageY - offset.top);

relX = relX - imageOffsets.x;
relY = relY - imageOffsets.y;

coords.setValue("(" + relX + "/" + relY + ")");
});
}
}());

5) To test the above logic, add a listener on image. In the demo loadimage listener was added on foundation image component widget /libs/foundation/components/image/dialog/items/image, which is bad; Ideally the foundation components should never be modified; did it for demonstration purposes only

6) Create node listeners (nt:unstructured) /libs/foundation/components/image/dialog/items/image/listeners, add a property loadimage with the following value (in other words, add this listener on html5 smart images needing the image mouse coordinates functionality)

function(image) { 
ExperienceAEM.showMouseCoordinates(image) ;
}






AEM 6 SP2 - Touch UI Open Component Dialog Programmatically

$
0
0

Goal


Open a Touch UI component dialog using sample action config - cq:actionConfigs. For the same in Classic UI follow this post

Demo | Package Install

Thank you Kaushal Mall for the solution - 2 below

A sample toolbar action for opening component dialog





Solution - 1


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-open-component-dialog

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-open-component-dialog/clientlib, set a property categories of String type to cq.authoring.dialog

3) Create file ( type nt:file ) /apps/touchui-open-component-dialog/clientlib/js.txt, add the following

                         open.js

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

(function ($, author) {
"use strict";

if (typeof window.ExperienceAEM == "undefined") {
window.ExperienceAEM = {};
}

ExperienceAEM.open = open;

function open(editable, param, target){
//Granite.author.store contains editables added on page
author.DialogFrame.openDialog(editable);
}
})($, Granite.author);

5) # 11 is for opening the component editable's dialog

6) Add a sample action config cq:actionConfigs, open action in the component's edit config cq:editConfig



7) Sample component'sopen dialog action config as xml - /apps/touchui-open-component-dialog/touchui-open-component-dialog/cq:editConfig

<?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" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:EditConfig">
<cq:actionConfigs jcr:primaryType="nt:unstructured">
<open
jcr:primaryType="nt:unstructured"
handler="ExperienceAEM.open"
icon="coral-Icon--game"
text="Open Dialog"/>
</cq:actionConfigs>
</jcr:root>

8) #7 ExperienceAEM.open handler (added in step 4) is executed on clicking the open toolbar action


Solution - 2


To show ootb component actions and custom action  - open dialog, check this adobe doc for more details

Package Install


  


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/touchui-open-component-dialog-register-action

2) Create node /apps/touchui-open-component-dialog-register-action/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.authoring.editor.hook

3) Create file (nt:file) /apps/touchui-open-component-dialog-register-action/clientlib/js.txt and add

                       open.js

4) Create file (nt:file) /apps/touchui-open-component-dialog-register-action/clientlib/open.js and add the following code

(function ($document, author) {
var openDialog = {
icon: 'coral-Icon--game',
text: 'Open Dialog',
handler: function (editable, param, target) {
author.DialogFrame.openDialog(editable);
},
condition: function (editable) {
return editable.type === "touchui-open-component-dialog-register-action/touchui-open-component-dialog";
},
isNonMulti: true
};

$document.on('cq-layer-activated', function (ev) {
if (ev.layer === 'Edit') {
author.EditorFrame.editableToolbar.registerAction('EAEM_OPEN_DIALOG', openDialog);
}
});
})($(document), Granite.author);



AEM 6 SP2 - Adding Dynamic Request Headers to Replication Requests

$
0
0

Goal


Replication Agent for Publish adds default headers Action, Path, Handle to request. Any custom headers can be added to requests by configuring Extended tab -> HTTP Headers of Publish Agent http://localhost:4502/etc/replication/agents.author/publish.html

This post is on adding dynamic headers to publish replication requests carried out using workflows. A process step introduced into workflow adds timestamp header eaem-unique-key to agent configuration before activation

Demo | Package Install | Source Code


Sample Request with Header eaem-unique-key





Solution


1) Create a Workflow Process OSGI Service apps.experienceaem.replication.SetUniqueKeyReplicationHeader, add the following code

package apps.experienceaem.replication;

import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.util.*;

@Component(metatype = false)
@Service
@Property(name = "process.label", value = "Experience AEM Unique Key Replication Header")
public class SetUniqueKeyReplicationHeader implements WorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(SetUniqueKeyReplicationHeader.class);

private static String PUBLISH_AGENT_CONFIG = "/etc/replication/agents.author/publish/jcr:content";
private static String PROTOCOL_HTTP_HEADERS = "protocolHTTPHeaders";
private static String EAEM_UNIQUE_KEY = "eaem-unique-key: ";

@Reference
private ResourceResolverFactory rrFactory;

public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
throws WorkflowException {
try {
Session session = workflowSession.getSession();

Map<String, Object> authInfo = new HashMap<String, Object>();
authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);

Resource res = rrFactory.getResourceResolver(authInfo).getResource(PUBLISH_AGENT_CONFIG);

if(res == null){
log.warn("Resource - " + PUBLISH_AGENT_CONFIG + ", not available");
return;
}

ValueMap vm = res.adaptTo(ValueMap.class);

String[] headers = vm.get(PROTOCOL_HTTP_HEADERS, String[].class);

headers = addUniqueKeyHeader(headers);

res.adaptTo(Node.class).setProperty(PROTOCOL_HTTP_HEADERS, headers);

session.save();
} catch (Exception e) {
throw new WorkflowException(e);
}
}

private String[] addUniqueKeyHeader(String[] headers){
if(ArrayUtils.isEmpty(headers)){
headers = new String[]{
"Action: {action}",
"Path: {path}",
"Handle: {path}",
EAEM_UNIQUE_KEY + new Date().getTime()
};

return headers;
}

for(int i = 0; i < headers.length; i++){
if(headers[i].startsWith(EAEM_UNIQUE_KEY)){
headers[i] = EAEM_UNIQUE_KEY + new Date().getTime();
}
}

return headers;
}
}

2) Add a Process Step in Request for Activation workflow (http://localhost:4502/cf#/etc/workflow/models/request_for_activation.html), configure it with apps.experienceaem.replication.SetUniqueKeyReplicationHeader (donot forget to save workflow)




3) Any activation requested through Request for Activation workflow now adds a eaem-unique-key header to the publish agent configuration (/etc/replication/agents.author/publish/jcr:content) just before activation (there might be synchronization issues when there is heavy replication using other workflows or direct activations )




4) A sample publish request with header eaem-unique-key

(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Sending POST request to http://localhost:4503/bin/receive?sling:authRequestLogin=1
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish sent. Response: 200 OK
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish ------------------------------------------------
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Sending message to localhost:4503
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> POST /bin/receive HTTP/1.0
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Action: Activate
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Path: /content/geometrixx/en
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Handle: /content/geometrixx/en
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> eaem-unique-key: 1430403556120
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> Referer: about:blank
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish >> ...spooling 56653 bytes...
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish --
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << HTTP/1.1 200 OK
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Date: Thu, 30 Apr 2015 14:19:16 GMT
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Content-Type: text/plain;charset=UTF-8
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Content-Length: 30
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << Server: Jetty(8.1.14.v20131031)
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish <<
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish << ReplicationAction ACTIVATE ok.
(com/day/cq/replication/job/publish)] com.day.cq.replication.Agent.publish Message sent.

5) Publish agent settings after the SetUniqueKeyReplicationHeader process step executes. The key eaem-unique-key changes with every activation

AEM 6 SP2 - Email Team on New Project Creation

$
0
0

Goal


New in AEM 6 is Projects http://localhost:4502/projects.html. Projects console can be used to add members, assign work, track progress etc. This post is on emailing team members when a project is created

Thank you ACS Commons for email code snippets

Demo | Package Install | Source Code


Project creation success modal




Email sent to a member




Solution


For this feature to work, CQ mailing service - com.day.cq.mailer.DefaultMailService should be configured with SMTP details. In the following steps we use Gmail for SMTP and a test account experience.aem@gmail.com as both sender and recipient (when project is created)


Create Gmail Account

1) Create a new gmail account for testing purposes, say experience.aem@gmail.com . Use this account as both sender and recipient

2) Make sure you tone down the security of test gmail account a bit, by clicking Turn on of Access for less secure apps (https://www.google.com/settings/security/lesssecureapps)





Set up CQ Mail Service

1) Configure the CQ Mail Service with Gmail SMTP credentials by accessing Felix config manager (http://localhost:4502/system/console/configMgr) or create a sling:osgiConfig for com.day.cq.mailer.DefaultMailService with these settings





2) If emails are not being sent by CQ Mailing service, make sure CQ can reach smtp.gmail.com. Check ports etc. In my case the problem was company vpn; if connected to vpn, CQ running on the machine could not connect to smtp.gmail.com

3) Configure some test accounts with email addresses





Download Project Api Jar

As of this writing, the api jar for Projects is not available in Adobe nexus repo (https://repo.adobe.com/nexus). For build purposes, copy jar com.adobe.cq.projects.api-0.0.14.jar from CQ install (search, find in author\crx-quickstart\launchpad\felix\bundle392\data\install) and copy it to local maven repo (windows - C:\Users\<user>\.m2\repository\com\adobe\cq\projects\com.adobe.cq.projects.api\0.0.14)





The Extension

1) Create a servlet apps.experienceaem.projects.SendProjectCreateEmail in CRX folder /apps/touchui-create-project-send-email for making the send email call when Send Email button is clicked (added in next steps), after project create

package apps.experienceaem.projects;

import com.adobe.cq.projects.api.Project;
import com.adobe.cq.projects.api.ProjectMember;
import com.day.cq.commons.mail.MailTemplate;
import com.day.cq.mailer.MessageGateway;
import com.day.cq.mailer.MessageGatewayService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Properties;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.UserManager;
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.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.jcr.base.util.AccessControlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.mail.internet.InternetAddress;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;

@Component(metatype = false, label = "Experience AEM Project Create Email Servlet", description = "")
@Service
@Properties({
@Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/send-project-create-email", propertyPrivate = true),
@Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)
})
public class SendProjectCreateEmail extends SlingAllMethodsServlet {

private static final Logger log = LoggerFactory.getLogger(SendProjectCreateEmail.class);

@Reference
private ResourceResolverFactory rrFactory;

@Reference
private MessageGatewayService messageGatewayService;

private static String TEMPLATE_PATH = "/apps/touchui-create-project-send-email/mail/template.html";
private static String SENDER_EMAIL = "experience.aem@gmail.com";
private static String SENDER_NAME = "Experience AEM";
private static String SENDER_EMAIL_ADDRESS = "senderEmailAddress";

public String sendMail(ResourceResolver resolver, Resource projectRes, String recipientEmail,
String recipientName){
if(StringUtils.isEmpty(recipientEmail)){
throw new RuntimeException("Empty email");
}

if(StringUtils.isEmpty(recipientName)){
recipientName = recipientEmail;
}

try{
Project project = projectRes.adaptTo(Project.class);
Map<String, String> emailParams = new HashMap<String,String>();

emailParams.put(SENDER_EMAIL_ADDRESS, SENDER_EMAIL);
emailParams.put("senderName", SENDER_NAME);
emailParams.put("projectName", project.getTitle());
emailParams.put("recipientName", recipientName);
emailParams.put("body","Project Created - <a href='http://localhost:4502/projects/details.html"
+ projectRes.getPath() + "'>" + project.getTitle() + "</a>");
emailParams.put("projectCreator", projectRes.adaptTo(ValueMap.class).get("jcr:createdBy", ""));

send(resolver, emailParams, recipientEmail);
}catch(Exception e){
log.error("Error sending email to " + recipientEmail, e);
recipientEmail = "";
}

return recipientEmail;
}

public Map<String, String> getMemberEmails(ResourceResolver resolver, Project project) throws Exception{
Map<String, String> members = new LinkedHashMap<String, String>();
String name = null, email = null;

UserManager um = AccessControlUtil.getUserManager(resolver.adaptTo(Session.class));
ValueMap profile = null; Iterator<Authorizable> itr = null;
List<Authorizable> users = new ArrayList<Authorizable>();

for(ProjectMember member : project.getMembers()) {
Authorizable user = um.getAuthorizable(member.getId());

if(user instanceof Group){
itr = ((Group)user).getMembers();

while(itr.hasNext()) {
users.add(itr.next());
}
}else{
users.add(user);
}
}

for(Authorizable user : users){
profile = resolver.getResource(user.getPath() + "/profile").adaptTo(ValueMap.class);

email = profile.get("email", "");

if(StringUtils.isEmpty(email)){
continue;
}

name = profile.get("familyName", "") + "" + profile.get("givenName", "");

if(StringUtils.isEmpty(name.trim())){
name = user.getID();
}

members.put(name, email);
}

return members;
}

private Email send(ResourceResolver resolver, Map<String, String> emailParams,
String recipientEmail) throws Exception{

MailTemplate mailTemplate = MailTemplate.create(TEMPLATE_PATH, resolver.adaptTo(Session.class));

if (mailTemplate == null) {
throw new Exception("Template missing - " + TEMPLATE_PATH);
}

Email email = mailTemplate.getEmail(StrLookup.mapLookup(emailParams), HtmlEmail.class);

email.setTo(Collections.singleton(new InternetAddress(recipientEmail)));
email.setFrom(SENDER_EMAIL);

MessageGateway<Email> messageGateway = messageGatewayService.getGateway(email.getClass());

messageGateway.send(email);

return email;
}

public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException{
ResourceResolver resolver = request.getResourceResolver();

String projectPath = request.getParameter("projectPath");

try{
if(StringUtils.isEmpty(projectPath)){
throw new RuntimeException("Empty projectPath");
}

Resource res = resolver.getResource(projectPath);

if(res == null){
throw new Exception("Project not found - " + projectPath);
}

Project project = res.adaptTo(Project.class);

Map<String, String> members = getMemberEmails(resolver, project);
String recipientEmail = null;
JSONArray output = new JSONArray();

for(Map.Entry<String, String> member : members.entrySet()){
recipientEmail = sendMail(resolver, res, member.getValue(), member.getKey());

if(StringUtils.isEmpty(recipientEmail)){
continue;
}

output.put(recipientEmail);
}

response.getWriter().print("{ success : " + output.toString() + " }");
}catch(Exception e){
log.error("Error sending email for project create - " + projectPath, e);
response.getWriter().print("{ error : 'error sending email' }");
}
}
}


2) #72 to #78 parameters are needed for email template, created in next step

3) #172 getMemberEmails() call uses Projects api to get a list of members, members in groups, addresses for sending emails

4) Create a template html /apps/touchui-create-project-send-email/mail/template.html for email body with following code. The ${} placeholders are replaced with sender and recipients emails

From: ${senderName} <${senderEmailAddress}>
Subject: ${projectName} project created

Hello ${recipientName}

${body}

From
${projectCreator}

5) Create clientlib (cq:ClientLibraryFolder) /apps/touchui-create-project-send-email/clientlib with categories cq.projects.admin.createprojectwizard

6) Create clienlib js file /apps/touchui-create-project-send-email/clientlib/send-email.js, add the following code

(function(window, $) {
var MODAL = "modal",
OPEN_PROJECT_TEXT = "Open project",
EMAIL_SERVLET = "/bin/experience-aem/send-project-create-email?projectPath=";

var modalPlugin = $.fn[MODAL],
ui = $(window).adaptTo("foundation-ui");

function emailTeam(path){
if(path.indexOf("/content") < 0){
return;
}

var projectPath = path.substring(path.indexOf("/content"));

$.ajax( EMAIL_SERVLET + projectPath).done(handler);

function handler(data){
if(data.success){
document.location = path;
return;
}

ui.alert("Error", "Error emailing team", "error");
}
}

//there could be many ways to intercept project creation ajax, i just thought the following is cleaner
function modalOverride(optionsIn){
modalPlugin.call(this, optionsIn);

var $element = $(this);

if($element.length == 0){
return;
}

var $openProject = $element.find(".coral-Button--primary");

if($openProject.html() != OPEN_PROJECT_TEXT){
return;
}

var path = $openProject.attr("href");

$openProject.attr("href", "").html("Email Team").click( function(){
emailTeam(path);
} ) ;
}

$.fn[MODAL] = modalOverride;
})(window, Granite.$);

7) AEM's create project submit function was added inside a closure in /libs/cq/gui/components/projects/admin/createprojectwizard/clientlibs/js/createprojectwizard.js and not available for extension. To capture the result of call, #51 intercepts the result of create project call, by extending the modal and making necessary changes to button, for sending email

AEM 6 SP2 - Change Overlay (Parsys) Border Color in Touch UI

$
0
0

Goal


Change the Overlay (Parsys) border color in Touch UI. If website's background has the same color as overlay or site CSS cannot contrast CQ parsys, the following simple css override might help

For Classic UI check this post

Package Install


Product Overlay




Extension Overlay





Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-change-overlay-color

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-change-overlay-color/clientlib and set a property categories of String type to cq.authoring.editor

3) Create file ( type nt:file ) /apps/touchui-change-overlay-color/clientlib/css.txt, add the following

                         change-overlay-color.css

4) Create file ( type nt:file ) /apps/touchui-change-overlay-color/clientlib/change-overlay-color.css, add the following styles

.cq-Overlay.is-active,
.cq-Overlay.is-hover,
.cq-Overlay.is-selected {
border-color: red;
color: red;
}

AEM 6 SP2 - Handling Custom Protocol in Link Href in Rich Text Editor

$
0
0

Goal


Adding protocols like tel: (or any custom) in anchor tag href attribute, may not be printed as entered in RTE as link checker com.day.cq.rewriter.linkchecker.impl.LinkCheckerImpl and XSS protection com.adobe.granite.xss.impl.HtmlToHtmlContentContext AntiSamy removes unrecognized protocols during component rendering. Here is the warning seen in error log

06.05.2015 10:07:45.213 *INFO* [0:0:0:0:0:0:0:1 [1430924865002] GET /content/geometrixx/en.html HTTP/1.1] com.adobe.granite.xss.impl.HtmlToHtmlContentContext AntiSamy warning: The a tag contained an attribute that we could not process. The href attribute had a value of "tel&#58;18475555555". This value could not be accepted for security reasons. We have chosen to remove this attribute from the tag and leave everything else in place so that we could process the input.

To get around this problem some configuration changes (Package Install) are required in CQ

Thank you Amrit Verma for the tip..

Solution


1) Overlay /libs/cq/xssprotection/config.xml in /apps - /apps/cq/xssprotection/config.xml

2) Add the protocol, say telURL

<regexp name="telURL" value="tel:[0-9]+"/>




3)  Add telURL configuration to the accepted list of anchor href

<attribute name="href">
<regexp-list>
<regexp name="onsiteURL"/>
<regexp name="offsiteURL"/>
<regexp name="telURL"/>
</regexp-list>
</attribute>



4) Add tel: to the Link Checker Special Link Prefixes http://localhost:4502/system/console/configMgr/com.day.cq.rewriter.linkchecker.impl.LinkCheckerImpl




5) With these configuration changes any tel: links in RTE should render fine...






AEM 61 - Touch UI Multiple Root Paths in Tags Picker

$
0
0

Goal


Support multiple root paths in Tags Picker of Touch UI. For Classic UI check this post

This is a picker extension only, searching inline still returns every tag available

Demo |  Package Install


Tags Picker





Tags Picker with Root Path Configuration (ootb)




Tags Picker with Root Path (ootb) rootPath - /etc/tags/geometrixx-outdoors/activity





Tags Picker with Multiple Root Paths Configuration (Extension)





Tags Picker with Multiple Root Paths (Extension) eaemTagsRootPaths -  /etc/tags/geometrixx-outdoors/activity, /etc/tags/geometrixx-media/entertainment






Solution


1) Login to CRXDE Lite http://localhost:4502/crx/de, create folder /apps/touchui-tags-picker-custom-root-paths

2) Create folder /apps/touchui-tags-picker-custom-root-paths/tags-picker and file /apps/touchui-tags-picker-custom-root-paths/tags-picker/tags-picker.jsp, add the following code. This picker jsp extends ootb tags picker jsp /libs/cq/gui/components/common/tagspicker to set the data source url /apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper.html (ootb its /libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html set in /libs/cq/gui/components/common/tagspicker/render.jsp). Had the pickerSrc attribute been available as configuration param like rootPath, this step would have not been required...

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="org.apache.commons.lang3.ArrayUtils" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<sling:include resourceType="/libs/cq/gui/components/common/tagspicker" />

<%
Config cfg = cmp.getConfig();
String[] eaemTagsPaths = cfg.get("eaemTagsRootPaths", String[].class);

//tags paths not set, continue with ootb functionality
if(ArrayUtils.isEmpty(eaemTagsPaths)){
return;
}
%>

<script type="text/javascript">
(function(){
var EAEM_TAGS_PATHS = "eaemtagsrootpaths",
BROWSER_COLUMN_PATH = "/apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper.html";

function changeTagsPickerSrc(){
var $eaemTagsPicker = $("[data-" + EAEM_TAGS_PATHS + "]");

if($eaemTagsPicker.length == 0){
return;
}

var browserCP = BROWSER_COLUMN_PATH + '<%=StringUtils.join(eaemTagsPaths, ",")%>';

$eaemTagsPicker.attr("data-picker-src", browserCP);
}

changeTagsPickerSrc();
}());
</script>

3) Create folder /apps/touchui-tags-picker-custom-root-paths/content and nt:unstructured node /apps/touchui-tags-picker-custom-root-paths/content/tag-column-wrapper with sling:resourceType /apps/touchui-tags-picker-custom-root-paths/tag-browser-column

4) To add the picker browser column renderer extension, create folder /apps/touchui-tags-picker-custom-root-paths/tag-browser-column and file /apps/touchui-tags-picker-custom-root-paths/tag-browser-column/tag-browser-column.jsp with the following code. It includes /libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html for getting the columns html and later removes unwanted root nodes

<%@ page import="org.apache.sling.api.request.RequestPathInfo" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%!
String TAG_BROWSER_COLUMN_PATH = "/libs/wcm/core/content/common/tagbrowser/tagbrowsercolumn.html";
String TAG_NAV_MARKER = "eaemTagNavMarker";

private String getParentTagPath(String tagPath) {
return tagPath.substring(0, tagPath.lastIndexOf("/"));
}

private JSONArray getTagPathsJson(String[] tagPaths){
JSONArray array = new JSONArray();

for(String tagPath: tagPaths){
array.put(tagPath);
}

return array;
}
%>

<%
RequestPathInfo pathInfo = slingRequest.getRequestPathInfo();
String tagPaths[] = pathInfo.getSuffix().split(",");

for(String tagPath: tagPaths){
String includePath = TAG_BROWSER_COLUMN_PATH + getParentTagPath(tagPath);

%>
<sling:include path="<%=includePath%>" />
<%
}
%>
<div id="<%=TAG_NAV_MARKER%>">
</div>

<script type="text/javascript">
(function(){
function removeAddnNavsGetColumn($navs){
$navs.not(":first").remove(); //remove all additional navs
return $navs.first().children(".coral-ColumnView-column-content").html("");//get the column of first nav
}

function addRootTags(){
var $tagMarker = $("#<%=TAG_NAV_MARKER%>"),
$navs = $tagMarker.prevAll("nav"),
tagPaths = <%=getTagPathsJson(tagPaths)%>,
rootTags = [];

//find the root tags
$.each(tagPaths, function(index, tagPath){
rootTags.push($navs.find("[data-value='" + tagPath + "']"));
});

removeAddnNavsGetColumn($navs).append(rootTags);

//remove the tag marker div
$tagMarker.remove();
}

addRootTags();
}());
</script>

5) In the component dialog add necessary configuration eaemTagsRootPaths and sling:resourceType /apps/touchui-tags-picker-custom-root-paths/tags-picker. For example, here is a sample configuration added on ootb page properties /libs/foundation/components/page/cq:dialog/content/items/tabs/items/basic/items/column/items/title/items/tags. This is for demonstration only, never touch /libs






AEM 6 SP2 - Accessing CRX Remotely using Jcr Remoting Based On Webdav (DavEx)

$
0
0

Goal


To access CRX remotely in a client java program, JCR Remoting (DavEx) or RMI can be used. This post is on using JCR Remoting (aka WebDav remoting, DavEx). For RMI check this post

More documentation:

http://wiki.apache.org/jackrabbit/RemoteAccess

https://docs.adobe.com/docs/en/crx/2-3/developing/accessing_the_crx.html

Solution


1) If maven is used, following are the dependencies

<dependencies>
<dependency>
<groupId>javax.jcr</groupId>
<artifactId>jcr</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.apache.jackrabbit</groupId>
<artifactId>jackrabbit-jcr2dav</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>

2) Without maven, to connect to CRX in a plain java standalone program, the following jars should be added to classpath. These jars can be downloaded from Adobe repo - Nexus

Assuming jars are available in user's .m2 folder, the following paths should be added to classpath

                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr-commons\2.7.0\jackrabbit-jcr-commons-2.7.0.jar
                      C:\Users\nalabotu\.m2\repository\javax\jcr\jcr\2.0\jcr-2.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr2dav\2.4.0\jackrabbit-jcr2dav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-jcr2spi\2.4.0\jackrabbit-jcr2spi-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi\2.4.0\jackrabbit-spi-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi2dav\2.4.0\jackrabbit-spi2dav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\slf4j\slf4j-api\1.7.5\slf4j-api-1.7.5.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-spi-commons\2.4.0\jackrabbit-spi-commons-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\org\apache\jackrabbit\jackrabbit-webdav\2.4.0\jackrabbit-webdav-2.4.0.jar
                      C:\Users\nalabotu\.m2\repository\commons-httpclient\commons-httpclient\3.1\commons-httpclient-3.1.jar
                      C:\Users\nalabotu\.m2\repository\commons-codec\commons-codec\1.6\commons-codec-1.6.jar
                      C:\Users\nalabotu\.m2\repository\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar
                      C:\Users\nalabotu\.m2\repository\commons-collections\commons-collections\3.2.1\commons-collections-3.2.1.jar

3) A sample standalone program for connecting to CRX using DavEx and execute query, returning templates (type cq:Template)

package apps;

import org.apache.jackrabbit.commons.JcrUtils;

import javax.jcr.*;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;

public class DavExWebDavRemotingTest {
public static void main(String[] args) throws Exception{
String REPO = "http://localhost:4502/crx/server";
String WORKSPACE = "crx.default";

Repository repository = JcrUtils.getRepository(REPO);

Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()), WORKSPACE);
QueryManager qm = session.getWorkspace().getQueryManager();

String stmt = "select * from cq:Template";
Query q = qm.createQuery(stmt, Query.SQL);

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

while(results.hasNext()){
node = (Node)results.next();
System.out.println(node.getPath());
}

session.logout();
}
}

AEM 6 SP2 - Query Builder Predicate Evaluator for ordering results Ignoring Case

$
0
0

Goal


Create a predicate evaluator for ordering Query Builder - /bin/querybuilder.json results based on node properties, Case Ignored...

Thanks to this blog post for providing insight on predicate evaluators and documentation on docs.adobe.com


So with the following query

                     http://localhost:4502/bin/querybuilder.json?p.limit=20&p.offset=0&eaem-ignore-case.property=jcr:content/metadata/uaDIO:reportName&orderby=eaem-ignore-case&fulltext=*.indd&p.hits=full&p.nodedepth=2&path=/content/dam/Product/Assortments&type=dam:Asset


prettified....

                     p.limit:  20
                     p.offset:  0
                     eaem-ignore-case.property:  jcr:content/metadata/uaDIO:reportName
                     orderby:  eaem-ignore-case
                     fulltext:  *.indd
                     p.hits:  full
                     p.nodedepth:  2
                     path:  /content/dam/Product/Assortments
                     type:  dam:Asset

eaem-ignore-case predicate evaluator getOrderByProperties() returns the property jcr:content/metadata/uaDIO:reportName adding necessary xpath function fn:upper-case and the final xpath query built would be....

                     /jcr:root/content/dam/Product/Assortments//element(*, dam:Asset)[jcr:contains(., '*.indd')] order by fn:upper-case(jcr:content/metadata/@uaDIO:reportName)


Solution


1) Create  a OSGI bundle with case insensitive predicate evaluator class, add the following code

package apps.experienceaem.pe;

import com.day.cq.search.Predicate;
import com.day.cq.search.eval.AbstractPredicateEvaluator;
import com.day.cq.search.eval.EvaluationContext;
import org.apache.felix.scr.annotations.Component;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.query.Row;
import java.util.*;

@Component(metatype = false, factory = "com.day.cq.search.eval.PredicateEvaluator/eaem-ignore-case")
public class CaseInsensitiveOrderByPredicate extends AbstractPredicateEvaluator {
private static final Logger logger = LoggerFactory.getLogger(CaseInsensitiveOrderByPredicate.class);

public static final String PROPERTY = "property";

public String[] getOrderByProperties(Predicate p, EvaluationContext context) {
Map<String, String> paramMap = p.getParameters();
List<String> orderProps = new ArrayList<String>();

for(String param : paramMap.values()){
orderProps.add("fn:upper-case(" + param + ")");
}

return orderProps.toArray(new String[0]);
}

/**
* can be used for further ordering, or scenarios where getOrderByProperties() isn't enough
*
* @param predicate
* @param context
* @return
*/
/*public Comparator<Row> getOrderByComparator(final Predicate predicate, final EvaluationContext context) {
return new Comparator<Row>() {
public int compare(Row r1, Row r2) {
int ret = 1;

if ((r1 == null) || (r2 == null) || (predicate.get(PROPERTY) == null)) {
return ret;
}

try {
ValueMap valueMap1 = context.getResource(r1).adaptTo(ValueMap.class);
ValueMap valueMap2 = context.getResource(r2).adaptTo(ValueMap.class);

String property1 = valueMap1.get(predicate.get(PROPERTY), "");
String property2 = valueMap2.get(predicate.get(PROPERTY), "");

ret = property1.compareToIgnoreCase(property2);
} catch (Exception e) {
logger.error(e.getMessage());
}

return ret;
}
};
}*/
}

2) Check registered evaluators - http://localhost:4502/system/console/services?filter=%28component.factory%3Dcom.day.cq.search.eval.PredicateEvaluator%2F*%29


AEM 6 SP2 - Classic UI Task Management Sort Task Projects Tree Nodes

$
0
0

Goal


Sort the Task Projects tree in Task Management console http://localhost:4502/libs/cq/taskmanagement/content/taskmanager.html#/tasks

Demo | Package Install


Product




Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-sort-task-projects

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-sort-task-projects/clientlib and set a property categories of String type to cq.taskmanagement

3) Create file ( type nt:file ) /apps/classicui-sort-task-projects/clientlib/js.txt, add the following

                         sort.js

4) Create file ( type nt:file ) /apps/classicui-sort-task-projects/clientlib/sort.js, add the following code

(function(){
var pathName = window.location.pathname;

if( pathName.indexOf("/libs/cq/taskmanagement/content/taskmanager.htm") != 0 ){
return;
}

var TREE_ID = "cq-taskmanager-tree";

function sort(treePanel, asc){
treePanel.on('load', function(node){
node.childNodes.sort(function(a,b){
a = a["text"].toLowerCase();
b = b["text"].toLowerCase();
return asc ? ( a > b ? 1 : (a < b ? -1 : 0) ) : ( a > b ? -1 : (a < b ? 1 : 0) ) ;
});
})
}

var INTERVAL = setInterval(function(){
var tree = CQ.Ext.getCmp(TREE_ID);

if(tree){
clearInterval(INTERVAL);
sort(tree, true);
}
}, 250);
}());


AEM 6 SP2 - Classic UI Restrict Moving Folder with N level Subfolders in Damadmin

$
0
0

Goal


Restrict moving folders with deep nesting and thousands of assets, triggering resource intensive processing in CRX

Demo | Package Install


Error when Subfolder Levels > 1 





Move in Tree Not Allowed




Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-restrict-folder-move

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-restrict-folder-move/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classicui-restrict-folder-move/clientlib/js.txt, add the following

                         restrict-move.js

4) Create file ( type nt:file ) /apps/classicui-restrict-folder-move/clientlib/restrict-move.js, add the following code

(function () {
if (window.location.pathname !== "/damadmin") {
return;
}

var NESTING_ALLOWED = 1, TREE = "cq-damadmin-tree";

/*//the original move dialog fn
var cqMoveDialog = CQ.wcm.Page.getMovePageDialog;

//override ootb function
CQ.wcm.Page.getMovePageDialog = function (path, isPage) {
var dialog = cqMoveDialog(path, isPage);

function handler(isNotAllowed) {
if (isNotAllowed) {
CQ.Ext.Msg.alert("Error", "Moving Folder with > " + NESTING_ALLOWED + " level subfolders, not allowed");
dialog.close();
}
}

isMoveAllowed(path).then(handler);

return dialog;
};*/

//the original move dialog fn
var cqMovePage = CQ.wcm.SiteAdmin.movePage;

//override ootb function
CQ.wcm.SiteAdmin.movePage = function () {
var selections = this.getSelectedPages();

if(selections.length == 0){
return;
}

var that = this;

function handler(isNotAllowed) {
if (isNotAllowed) {
CQ.Ext.Msg.alert("Error", "Moving Folder with > " + NESTING_ALLOWED + " level subfolders, not allowed");
return;
}

cqMovePage.call(that);
}

isMoveAllowed(selections[0].id).then(handler);
};

var INTERVAL = setInterval(function(){
var tree = CQ.Ext.getCmp(TREE);

if(tree){
clearInterval(INTERVAL);
handleTreeNodeMove(tree);
}
}, 250);

function getFolderJson(path) {
return $.ajax( path + "." + (NESTING_ALLOWED + 1) + ".json" );
}

function isMoveAllowed(path) {
function handler(data) {
return reachedMaxNestedLevel(data, NESTING_ALLOWED);
}

function reachedMaxNestedLevel(folder, nestingNum) {
for (var x in folder) {
if (!folder.hasOwnProperty(x) || !isFolder(folder[x])) {
continue;
}

if (nestingNum == 0) {
return true;
}

if (reachedMaxNestedLevel(folder[x], nestingNum - 1)) {
return true;
}
}

return false;
}

return getFolderJson(path).then(handler);
}

function handleTreeNodeMove(tree) {
var listeners = tree.initialConfig.listeners;

tree.removeListener("beforenodedrop", listeners.beforenodedrop, tree);

tree.on("beforenodedrop", function (dropEvent) {
CQ.Ext.Msg.alert("Error", "Moving tree nodes not allowed, use Move... in grid");
return false;
});
}

function isFolder(node) {
return node["jcr:primaryType"] == "sling:OrderedFolder";
}
}());


AEM - Source Code, Code Reviews, Build and Release Management

$
0
0

Goal


Every other's SCM is icky. So you may find the following process lengthy, confusing and lead to the decision, let's continue with the way it is - and that is fine given the nature of project.

Here is a process (not fork based) on setting up the project source code GIT repo in Bit Bucket (free for 5 users), Source Tree GIT Client, Git Flow for Branch Management and creating Releases, Hotfixes, in a collaborative environment. This article does not discuss Continuous Integration

The process discussed below is based on GIT Flow and suggests creating 5 type of branches

                         Development branch -  develop
                         Production branch - master
                         Feature branches - prefixed with feature/
                         Release branches - prefixed with release/
                         Hotfix branches - prefixed with hotfix/

Solution


Create Repository in BitBucket

1) Sign up for creating a Repository. The admin, for example, experience.aem@gmail.com (username: eaem) signs up and creates a repo by clicking Create -> Create repository




2) Enter project name - experience-aem-intranet and other necessary details





Clone Repository

1) Create a local clone by accessing https://bitbucket.org/eaem/experience-aem-intranet/overview clicking on Clone in Source Tree




2) In Source Tree, enter local directory path, click Clone




3) Master branch clone gets created on the file system and bookmarked in source tree



4) If you are using multiple bitbucket accounts in source tree, add the credentials in experience-aem-intranet repo settings




Initialize Repo with GITFlow

1) Create an empty file, eg. readme.txt in local repo - C:\dev\code\projects\cq6-extensions\experience-aem-intranet\readme.txt; commit and push, to create local & remote (origin) master branch



2) Using GIT Flow gives a neat, standardized approach; streamlines the entire release process and works well in collaborative environments. The next step is to initialize repo experience-aem-intranet with GIT Flow



2) Branch develop gets created


3) The significance of master branch is, it contains released/deployed codebase (the one installed on production). develop branch contains feature code, bug fixes. Quality Assurance team should generally get their deployments built out of develop branch, for feature testing.

So its good to have some write access control on master and develop branches to avoid accidental checkins, make it a policy to review any code ready for QA or Production. In this example, the project admin eaem acts as reviewer. The job of a reviewer is to make sure feature/bugfix code submitted for review is clean, conventions followed, proper commenting added, any best practices internally are followed etc...

4) For code review process, set access restrictions on master and develop branches by accessing bitbucket https://bitbucket.org/eaem/experience-aem-intranet/admin



5) This being a private repository, only team members should be allowed to access source code for feature development. For this example, lets add user nalabotu - https://bitbucket.org/eaem/experience-aem-intranet/admin/access





Create New AEM Bundle

1) Admin of experience-aem-intranet eaem creates new module intranet-portal using archetype https://github.com/Adobe-Marketing-Cloud/aem-project-archetype

C:\dev\code\projects\cq6-extensions\experience-aem-intranet>"C:\Program Files\Java\jdk1.7.0_25\bin\java" -Dmaven.home=C:\dev\code\install\apache-maven-3.1.0 -Dclassworlds.conf=C:\dev\code\install\apache-maven-3.1.0\bin\m2.conf -Dfile.encoding=UTF-8 -classpath C:\dev\code\install\apache-maven-3.1.0\boot\plexus-classworlds-2.4.2.jar org.codehaus.classworlds.Launcher --fail-fast --lax-checksums -DinteractiveMode=false 
-DgroupId=com.experienceaem.intranet
-DartifactId=intranet-portal
-Dversion=1.0-SNAPSHOT
-DarchetypeGroupId=com.adobe.granite.archetypes
-DarchetypeArtifactId=aem-project-archetype
-DarchetypeVersion=10
-Dpackage=com.experienceaem
-DappsFolderName=experienceaem-intranet
"-DartifactName=Experience AEM Intranet Portal"
"-DcomponentGroupName=Experience AEM"
-DcontentFolderName=experience-aem-intranet
-DcssId=experience-aem
"-DpackageGroup=Experience AEM"
"-DsiteName=Experience AEM Intranet" org.apache.maven.plugins:maven-archetype-plugin:RELEASE:generate


2) The project module experience-aem-intranet\intranet-portal in IDE



3) Install module package using mvn -PautoInstallPackage clean install and a sample page http://localhost:4502/content/experience-aem-intranet/en.html should become available



4)  Add target (folder name) to C:\dev\code\projects\cq6-extensions\experience-aem-intranet\.gitignore, as folders generated via build are not needed in repository

5) The current branch is develop (if it isn't double click to switch to develop ), commit and push the code to origin develop branch



6) At this point the module Intranet Portal is available for feature development


Feature Development (Bug Fixing)

1) Say a new feature story is assigned to user nalabotu (developing on MAC). User starts development by creating a feature branch feature/EAEM-users-json-servlet-sreek  (<Project-Name>-<Brief-Description>-<Username>)





2) Code the servlet com.experienceaem.core.servlets.GetUsersJSON, test it by accessing http://localhost:4502/bin/experience-aem/users.json, commit and push from local feature branch to origin (select create pull request)




   Before creating the pull request make sure you pull changes from remote develop and merge the latest develop changes into feature branch. It's a good practice to merge latest code into feature branches daily or should do it atleast before creating a pull to develop request, test and make sure feature changes are working ok, with latest develop code




3) The remote branch should have been created - feature/EAEM-users-json-servlet-sreek



4) In Step 2, user has selected Create pull request while committing; a browser tab with create pull request opens, make sure the feature to be merged is on left and destination develop on right, select the reviewer eaem, click Create pull request




5) The pull request gets created and email sent to reviewers



5) In regular GIT Flow, developer clicks Finish feature in source tree and the feature branch gets merged to local develop, which can then be pushed to origin develop. For review process, admin eaem has push restrictions enabled on develop and master, so Finish feature is not going to work here, as the developer nalabotu with no write access to remote develop,cannot push the local develop changes. Doing so will result in following error; so when the developer is done with coding a feature, he/she can simply delete the branch (instead of clicking Finish feature), after creating the pull request and it gets merged to develop branch by reviewer.





Review Process - admin eaem

1) User eaem, clicks on the review link in email notification, adds a comment (developer nalabotu is notified by email)




Incorporate Review Comments - dev nalabotu

1) Developer implements review comments, commits, pushes & creates a pull request as explained above; the pull request created earlier gets updated with latest changes





Reviewer merges the changes - eaem

1) Reviewer eaem is happy with the implementation, clicks Merge to pull in the feature changes into develop. At this point the feature is ready for testing by quality team



2) Hooks can be configured in bitbucket on branches (develop, master etc.) to kickoff build and deploy packages to test CQ servers, when any changes are pulled into develop branch (Continuous Integration is not discussed in this article)




Creating Release

1) When feature development/testing is done and code is all set to move to production, the project admin (or any user with necessary permissions on develop, master and release branches) uses GIT Flow -> Create release to create a release, say v1.1.0 (based on develop)





2) Make the necessary version changes in pom.xml, 1.1.0 (following the <Major>.<Minor>.<Patch> convention) test on local CQ, commit and push the changes to origin. Some use even/odd convention, even number in minor for release and odd number for development, but for pre-release or development, -SNAPSHOTcan be used in version; so 1.2.3-snapshot would be a lower version number than 1.2.3, for more information check this post)




3) The release branch release/v1.1.0, created in origin (remote)...



4) Click Finish Release to create the release and tag it with some message




5) The release changes (versions in pom.xml) are merged to master and develop branches. Push to origin...



6) Switch to master branch, build and do some smoke testing on local CQ. Upload the deployment package to a centralized location for Production deployment. Develop branch has latest code and Master branch has code deployed to Production. 


Prepare Develop for Next Iteration

1) Release 1.1.0 was created, code was promoted to master, its time to prepare develop for next set of features

2) Project admin eaem, switches to develop branch and makes changes to version number in pom.xml , 1.2.0-SNAPSHOT (So the next release is 1.2.0). Commit and Push to Origin



3) Send an email to developers to merge the latest develop changes into their feature branches to update them with latest version numbers


Production Bugs HotFixes

1) If you need to patch the latest release without picking up new features from the development branch, you can create a hotfix branch from the latest deployed code in master. Once you’ve made your changes, the hotfix branch is then merged back into both the master branch (to update the released version) and the development branch (to make sure the fixes go into the next release too)

2) Say the servlet GetUsersJSON coded by developer nalabotu, has a bug. It was supposed to return userids and not user node paths; the bug was assigned to developer(admin) eaem.

3) Project admin eaem setups a hotfix branch by using GIT Flow -> Start New HotFix



4) Say every end of month, bug fixes are promoted to production via hot fix branch. Name the hotfix may-bug-fixes. Hotfix branches are always based on master (production code)



5) Developers working on production bugfixes do not create feature branches. Fetch hotfix branch and bug fixes from all developers are pushed to one hotfix branch, here it's may-bug-fixes. Quality team creates a build out of hotfix branch, verifies fixes for sign-off.

6) Developer eaem fixes the servlet and pushes code to origin hotfix branch (similarly over the next few days, developers working on production bugs, sync hotfix branch, fix bugs and push the code to remote hotfix branch)



7) When the fixes are tested and ready to move to production, project admin, here eaem, or any developer having write permissions on master and develop branches, finishes the hotfix by clicking Git Flow -> Finish Hotfix





8) The hotfix branch gets merged back into master and develop branches. Push the changes, if there are any merge conflicts (say the same lines of code were modified in develop as well), resolve the conflicts...



9) Switch to master, generate the deployment packages, smoke test and upload the packages to a centralized location for deployment team

10) When the hotfix is finished, the bug fixes for production are automatically pulled (merged) into develop branch and available in next feature releases as well.



Viewing all 525 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>