Quantcast
Viewing all 526 articles
Browse latest View live

AEM 61 - Classic UI Composite Multifield Storing Values as Nodes

Goal


Create a Classic UI Composite Multifield, storing the composite field values as child nodes (useful when executing search queries for exact matches).

For storing values in json format, check this post

For Touch UI Composite Multifield storing values as child nodes check this post

Tested on AEM 61; should work ok on 60 and 561 too...  Demo | Package Install


Multi Field Panel


Image may be NSFW.
Clik here to view.


Value Nodes in CRX


Image may be NSFW.
Clik here to view.



Dialog in CRX


Image may be NSFW.
Clik here to view.



Dialog XML

#31, #37, #43 - property dName used in creating fully qualified name (./<multifield-name>/<order>/<dName>) eg ./stock/1/year

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="Multi Field"
xtype="dialog">
<items
jcr:primaryType="cq:Widget"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Panel"
title="Add">
<items jcr:primaryType="cq:WidgetCollection">
<stock
jcr:primaryType="cq:Widget"
hideLabel="false"
name="./stock"
title="Stock"
xtype="multifield">
<fieldConfig
jcr:primaryType="cq:Widget"
border="true"
hideLabel="true"
layout="form"
padding="10px"
width="1000"
xtype="multi-field-panel">
<items jcr:primaryType="cq:WidgetCollection">
<product-year-value
jcr:primaryType="cq:Widget"
dName="year"
fieldLabel="Year"
width="60"
xtype="textfield"/>
<product-price-value
jcr:primaryType="cq:Widget"
dName="price"
fieldLabel="Price"
width="60"
xtype="textfield"/>
<product-version-value
jcr:primaryType="cq:Widget"
dName="version"
fieldLabel="Path to Version"
xtype="pathfield"/>
</items>
</fieldConfig>
</stock>
</items>
</tab1>
</items>
</items>
</jcr:root>


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-multi-field-panel-node-store

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-multi-field-panel-node-store/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-multi-field-panel-node-store/clientlib/js.txt, add the following

                         multi-field.js

4) Create file ( type nt:file ) /apps/classic-ui-multi-field-panel-node-store/clientlib/multi-field.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.MultiFieldPanel = CQ.Ext.extend(CQ.Ext.Panel, {
constructor: function(config){
config = config || {};
ExperienceAEM.MultiFieldPanel.superclass.constructor.call(this, config);
},

initComponent: function () {
ExperienceAEM.MultiFieldPanel.superclass.initComponent.call(this);

function addName(items, prefix, counter){
items.each(function(i){
if(!i.hasOwnProperty("dName")){
return;
}

i.name = prefix + "/" + (counter) + "/" + i.dName;

if(i.el && i.el.dom){ //form serialization workaround
i.el.dom.name = prefix + "/" + (counter) + "/" + i.dName;
}
},this);
}

var multi = this.findParentByType("multifield"),
multiPanels = multi.findByType("multi-field-panel");

addName(this.items, this.name, multiPanels.length + 1);

multi.on("removeditem", function(){
multiPanels = multi.findByType("multi-field-panel");

for(var x = 1; x <= multiPanels.length; x++){
addName(multiPanels[x-1].items, multiPanels[x-1].name, x);
}
});
},

afterRender : function(){
ExperienceAEM.MultiFieldPanel.superclass.afterRender.call(this);

this.items.each(function(){
if(!this.contentBasedOptionsURL
|| this.contentBasedOptionsURL.indexOf(CQ.form.Selection.PATH_PLACEHOLDER) < 0){
return;
}

this.processPath(this.findParentByType('dialog').path);
})
},

getValue: function () {
var pData = {};

this.items.each(function(i){
if(!i.hasOwnProperty("dName")){
return;
}

pData[i.dName] = i.getValue();
});

return pData;
},

setValue: function (value) {
var counter = 1, item,
multi = this.findParentByType("multifield"),
multiPanels = multi.findByType("multi-field-panel");

if(multiPanels.length == 1){
item = value[counter];
}else{
item = value;
}

this.items.each(function(i){
if(!i.hasOwnProperty("dName")){
return;
}

i.setValue(item[i.dName]);
});

if(multiPanels.length == 1){
while(true){
item = value[++counter];

if(!item){
break;
}

multi.addItem(item);
}
}
},

validate: function(){
return true;
},

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

CQ.Ext.reg("multi-field-panel", ExperienceAEM.MultiFieldPanel);

5) A sample exact match query performed with query builder, on property name year, returning multifield nodes

http://localhost:4502/bin/querybuilder.json?property=year&property.value=2010&p.hits=full

Image may be NSFW.
Clik here to view.



AEM 61 - Touch UI Composite Multifield Store Values as Child Nodes

Goal


Create a Touch UI Composite Multifield, storing field values as child nodes (useful when executing search queries for exact matches).

For storing values as json check this post

For Classic UI Composite Multifield , storing values as child nodes check this post

Tested on AEM 61; should work ok on 60

Demo | Package Install


Composite Multifield in Dialog


Image may be NSFW.
Clik here to view.



Value Nodes in CRX


Image may be NSFW.
Clik here to view.



Dialog Structure


Image may be NSFW.
Clik here to view.




Dialog XML

Sample dialog with 3 composite multifields added in 3 tabs. #49, #125, #202 mark these multifields as composite, by specifying the flag eaem-nested

<?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/tabs"
type="nav"/>
<items jcr:primaryType="nt:unstructured">
<india
jcr:primaryType="nt:unstructured"
jcr:title="India"
sling:resourceType="granite/ui/components/foundation/section">
<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="India Dashboard"
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">
<dashboard
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Dashboard name"
fieldLabel="Dashboard"
name="./iDashboard"/>
<pages
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
eaem-nested=""
fieldDescription="Click '+' to add a new page"
fieldLabel="URLs">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
name="./iItems">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<page
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Page Name"
fieldLabel="Page Name"
name="./page"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
</items>
</column>
</items>
</field>
</pages>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</india>
<usa
jcr:primaryType="nt:unstructured"
jcr:title="USA"
sling:resourceType="granite/ui/components/foundation/section">
<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="USA Dashboard"
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">
<dashboard
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Dashboard name"
fieldLabel="Dashboard"
name="./uDashboard"/>
<pages
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
eaem-nested=""
fieldDescription="Click '+' to add a new page"
fieldLabel="URLs">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
eaem-nested=""
name="./uItems">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<page
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Page Name"
fieldLabel="Page Name"
name="./page"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
</items>
</column>
</items>
</field>
</pages>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</usa>
<uk
jcr:primaryType="nt:unstructured"
jcr:title="UK"
sling:resourceType="granite/ui/components/foundation/section">
<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="UK Dashboard"
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">
<dashboard
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Dashboard name"
fieldLabel="Dashboard"
name="./ukDashboard"/>
<pages
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
eaem-nested=""
fieldDescription="Click '+' to add a new page"
fieldLabel="URLs">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
eaem-nested=""
name="./ukItems">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<page
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Page Name"
fieldLabel="Page Name"
name="./page"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
</items>
</column>
</items>
</field>
</pages>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</uk>
</items>
</content>
</jcr:root>


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touch-ui-composite-multi-field-store-as-child-nodes

2) Create clientlib (type cq:ClientLibraryFolder/apps/touch-ui-composite-multi-field-store-as-child-nodes/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

3) Create file ( type nt:file ) /apps/touch-ui-composite-multi-field-store-as-child-nodes/clientlib/js.txt, add the following

                         multifield.js

4) Create file ( type nt:file ) /apps/classic-ui-multi-field-panel-node-store/clientlib/multifield.js, add the following code

(function () {
var DATA_EAEM_NESTED = "data-eaem-nested";
var CFFW = ".coral-Form-fieldwrapper";

//reads multifield data from server, creates the nested composite multifields and fills them
function addDataInFields() {
function getMultiFieldNames($multifields){
var mNames = {}, mName;

$multifields.each(function (i, multifield) {
mName = $(multifield).children("[name$='@Delete']").attr("name");

mName = mName.substring(0, mName.indexOf("@"));

mName = mName.substring(2);

mNames[mName] = $(multifield);
});

return mNames;
}

function buildMultiField(data, $multifield, mName){
if(_.isEmpty(mName) || _.isEmpty(data)){
return;
}

_.each(data, function(value, key){
if(key == "jcr:primaryType"){
return;
}

$multifield.find(".js-coral-Multifield-add").click();

_.each(value, function(fValue, fKey){
if(fKey == "jcr:primaryType"){
return;
}

var $field = $multifield.find("[name='./" + fKey + "']").last();

if(_.isEmpty($field)){
return;
}

$field.val(fValue);
});
});
}

$(document).on("dialog-ready", function() {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var mNames = getMultiFieldNames($multifields),
$form = $(".cq-dialog"),
actionUrl = $form.attr("action") + ".infinity.json";

$.ajax(actionUrl).done(postProcess);

function postProcess(data){
_.each(mNames, function($multifield, mName){
buildMultiField(data[mName], $multifield, mName);
});
}
});
}

//collect data from widgets in multifield and POST them to CRX
function collectDataFromFields(){
function fillValue($form, fieldSetName, $field, counter){
var name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

//remove the field, so that individual values are not POSTed
$field.remove();

$('<input />').attr('type', 'hidden')
.attr('name', fieldSetName + "/" + counter + "/" + name)
.attr('value', $field.val())
.appendTo($form);
}

$(document).on("click", ".cq-dialog-submit", function () {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var $form = $(this).closest("form.foundation-form"),
$fieldSets, $fields;

$multifields.each(function(i, multifield){
$fieldSets = $(multifield).find("[class='coral-Form-fieldset']");

$fieldSets.each(function (counter, fieldSet) {
$fields = $(fieldSet).children().children(CFFW);

$fields.each(function (j, field) {
fillValue($form, $(fieldSet).data("name"), $(field).find("[name]"), (counter + 1));
});
});
});
});
}

$(document).ready(function () {
addDataInFields();
collectDataFromFields();
});
})();

5) A sample exact match query performed with query builder, on property name page, returning composite multifield nodes

Image may be NSFW.
Clik here to view.

AEM 61 - Classic UI Nested Composite Multifield Panel

Goal


Create a Classic UI Nested Composite Multifield Panel. The logic for nested composite multifield and composite multifield is same; this post just has a copy of composite multifield code with additional dialog configuration required for nested multifield

For Touch UI Nested Composite Multifieldcheck this post

Demo | Package Install


Nested Composite Multifield

Image may be NSFW.
Clik here to view.


Values Stored in CRX as JSON


Image may be NSFW.
Clik here to view.



Dialog

Image may be NSFW.
Clik here to view.



Dialog as XML

#27 specifies the xtype multi-field-panel, for parent composite multifield, #60 the child composite multifield

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:Dialog"
title="Multi Field"
xtype="dialog">
<items
jcr:primaryType="cq:Widget"
xtype="tabpanel">
<items jcr:primaryType="cq:WidgetCollection">
<tab1
jcr:primaryType="cq:Panel"
title="Add">
<items jcr:primaryType="cq:WidgetCollection">
<map
jcr:primaryType="cq:Widget"
hideLabel="false"
name="./map"
title="Map"
xtype="multifield">
<fieldConfig
jcr:primaryType="cq:Widget"
border="true"
hideLabel="true"
layout="form"
padding="10px"
width="1000"
xtype="multi-field-panel">
<items jcr:primaryType="cq:WidgetCollection">
<product-year-value
jcr:primaryType="cq:Widget"
dName="year"
fieldLabel="Year"
width="60"
xtype="textfield"/>
<product-price-value
jcr:primaryType="cq:Widget"
dName="price"
fieldLabel="Price"
width="60"
xtype="textfield"/>
<product-version-value
jcr:primaryType="cq:Widget"
dName="version"
fieldLabel="Path to Version"
xtype="pathfield"/>
<product-region-multifield
jcr:primaryType="cq:Widget"
dName="region"
fieldLabel="Region"
hideLabel="false"
title="Add Regions"
xtype="multifield">
<fieldConfig
jcr:primaryType="cq:Widget"
border="true"
hideLabel="true"
layout="form"
padding="10px"
width="1000"
xtype="multi-field-panel">
<items jcr:primaryType="cq:WidgetCollection">
<product-country
jcr:primaryType="cq:Widget"
dName="country"
fieldLabel="Country"
width="60"
xtype="textfield"/>
<product-state
jcr:primaryType="cq:Widget"
dName="state"
fieldLabel="State"
width="60"
xtype="textfield"/>
</items>
</fieldConfig>
</product-region-multifield>
</items>
</fieldConfig>
</map>
</items>
</tab1>
</items>
</items>
</jcr:root>


Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classic-ui-nested-multi-field-panel

2) Create clientlib (type cq:ClientLibraryFolder/apps/classic-ui-nested-multi-field-panel/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classic-ui-nested-multi-field-panel/clientlib/js.txt, add the following

                         multi-field.js

4) Create file ( type nt:file ) /apps/classic-ui-nested-multi-field-panel/clientlib/multi-field.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.MultiFieldPanel = CQ.Ext.extend(CQ.Ext.Panel, {
panelValue: '',

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

initComponent: function () {
ExperienceAEM.MultiFieldPanel.superclass.initComponent.call(this);

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

this.add(this.panelValue);

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

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

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

},

afterRender : function(){
ExperienceAEM.MultiFieldPanel.superclass.afterRender.call(this);

this.items.each(function(){
if(!this.contentBasedOptionsURL
|| this.contentBasedOptionsURL.indexOf(CQ.form.Selection.PATH_PLACEHOLDER) < 0){
return;
}

this.processPath(this.findParentByType('dialog').path);
})
},

getValue: function () {
var pData = {};

this.items.each(function(i){
if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){
return;
}

pData[i.dName] = i.getValue();
});

return $.isEmptyObject(pData) ? "" : JSON.stringify(pData);
},

setValue: function (value) {
this.panelValue.setValue(value);

var pData = JSON.parse(value);

this.items.each(function(i){
if(i.xtype == "label" || i.xtype == "hidden" || !i.hasOwnProperty("dName")){
return;
}

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

validate: function(){
return true;
},

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

CQ.Ext.reg("multi-field-panel", ExperienceAEM.MultiFieldPanel);

AEM - Continuous Integration with Jenkins

Goal


For Source Code Management using GIT (Bitbucket) check this post

Jenkins is a continuous integration tool (CI) for automating builds. In simple terms, developers in a AEM project code a feature or bugfix, test on their local instances, commit/push to a central SVN or GIT repo; continuous integration tools like Jenkins kick off, build packages and deploy to some common AEM test/integration servers. Quality team can then test the feature/bug fix on AEM integration server

In a nutshell...

1) Developer starts working on a feature/bug-fix, marks the story in JIRA as In Progress
2) Tests the code on local AEM
3) Commits/Pushes change to the SVN/GIT repo. For source code management using GIT check this post
4) A configured Jenkins Hook in GIT can kickoff the build, deploy packages to AEM Integration Server. When too many changes are being pushed to the repo, admin may choose to manually start builds through Jenkins console (Jobs), to refresh integration environments.
5) Developer moves the story to QA
6) Quality team picks up the story and tests code changes on Integration Server.

Build Demo


Install Jenkins

1) Get Jenkins for Windows here

2) Run install with default settings. For more refined steps check this link

2) When completed, service Jenkins is available and a browser window opens up with url http://localhost:8080/

Image may be NSFW.
Clik here to view.




Configure Jenkins Global Security

1) After installation, by default, no authentication is required for accessing jenkins console. So it allows anyone create a job, right away


Image may be NSFW.
Clik here to view.

2) To secure Jenkins, enable Global Security (Manage Jenkins -> Configure Global Security) http://localhost:8080/configureSecurity/

Image may be NSFW.
Clik here to view.

3)  A Jenkins internal database of users can be created by selecting Jenkins’ own user database option or connect to organization LDAP. In the following example, Jenkins was connected to ldap on localhost:389 (a sample OpenLDAP database). So any logged-in user can modify the configuration, create jobs etc, a more fine grained access control can be set by selecting Matrix-based security

Image may be NSFW.
Clik here to view.


4) Two sample users eaem, nalabotu were created in local OpenLDAP database

Image may be NSFW.
Clik here to view.



JDK, GIT, Maven Configuration

1) Access the configuration screen, Manage Jenkins -> Configure System (http://localhost:8080/configure)


Image may be NSFW.
Clik here to view.


2) Configure the JDK used for compiling sources

Image may be NSFW.
Clik here to view.


3) Configure the GIT plugin to download sources from remote repository. For this post, use sample repo experience-aem-intranetcreated in this post

Image may be NSFW.
Clik here to view.




Image may be NSFW.
Clik here to view.


4) Add the path to GIT executable on file system, used for checking out source code


Image may be NSFW.
Clik here to view.


5) Configure MAVEN install, required for running any typical AEM project build

Image may be NSFW.
Clik here to view.


6) Restart Jenkins service


Creating Build Jobs

1) Create new job (If not already logged-in, login as user, say eaem)

Image may be NSFW.
Clik here to view.


2) Enter name experience-aem-intranet-portal and select the project type Maven

Image may be NSFW.
Clik here to view.


3) Configure the GIT repository url and credentials. The Branches to build specifies which branch of the repo should be downloaded and compiled, here its develop


Image may be NSFW.
Clik here to view.

4) Specify the relative path of pom.xml; the goal autoInstallPackage builds, installs packages to provided CQ instance, here its localhost

                 clean install -X -P autoInstallPackage -Dcrx.host=localhost -Dcrx.port=4502 -Dcrx.user=admin -Dcrx.password=admin -Dvault.timeout=30


Image may be NSFW.
Clik here to view.



5) Run the build by clicking Build Now

Image may be NSFW.
Clik here to view.

6) Build in progress; latest develop branch sources checked out

Image may be NSFW.
Clik here to view.


    Commit in SourceTree

Image may be NSFW.
Clik here to view.


7) Build creates a workspace with sources downloaded from remote GIT repo

Image may be NSFW.
Clik here to view.


8) Build successful...

Image may be NSFW.
Clik here to view.


8) Packages Installed on AEM running on localhost:4502

Image may be NSFW.
Clik here to view.


9) If the GIT repo can communicate with Jenkins, a web hook can be configured in GIT to trigger a build automatically when there is a commit on build branch say develop


AEM 61 - Random JCR Queries

JCR-SQL2


1)Get global collections

SELECT * FROM [nt:unstructured] WHERE [sling:resourceType] = 'dam/collection'


2) Get Project specific collections

SELECT * FROM [nt:unstructured] WHERE [sling:resourceType] = 'cq/gui/components/projects/admin/card/projectcard' and NAME() = '150609_belk_fisharmvntbb';


3) Get nodes (DAM Assets) with specified name

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd'


4) Get nodes (DAM Assets) with specified name, order by path

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd' ORDER BY 'jcr:path'


5) Get nodes (DAM Assets) with specified name, order by last modified date descending

SELECT * FROM [dam:Asset] where NAME([dam:Asset]) = 'one.indd' ORDER BY 'jcr:content/jcr:lastModified' desc


AEM 61 - Offloading DAM Upload Asset Workflow Process

Goal


Create a Master/Worker Author AEM Topology for offloading the asset upload post process, to a worker instance. So the worker instance is specifically configured to execute DAM Update Asset Workflow steps on assets uploaded to master instance.

1) Master is configured with DAM Update Asset Offloading Workflow
2) Users never see the worker author and always upload assets to master author
3) When assets are uploaded, a replication agent on master sends the asset package to worker
4) Worker executes DAM Update Asset Workflow on asset
5) Using reverse replication, the package with processed asset and renditions is replicated to master from worker

Clearly, processing too many heavy DAM assets in the author AEM instance (also used for authoring web pages) can drastically bring down performance, so offloading DAM Update Asset Workflow in such cases is considered wise....

Check adobe documentation for more information on Offloading Jobs & Sling Discovery features. The following steps are for local setup, so offloading replication agents use admin credentials and not a dedicated replication user.

Demo

Solution


1) Assuming there are two AEM author instances running on two separate machines (windows & mac) in same network...

                         Host                      IP Address                  OS                        Type                        URL

                         nalabotu-w7         192.168.0.3                  Windows              Master                       http://nalabotu-w7:4502/welcome
                         nalabotu-osx         192.168.0.8                   Macintosh            Worker                      http://nalabotu-osx:4502/welcome
                       
2) The first step is to add worker (nalabout-osx) in master (nalabotu-w7) topology. Here is the topology configuration on master before adding a worker; one instance exists with sling id ab3f05cd-1669-4c8a-b538-e24f6b37cfdf

                            http://nalabotu-w7:4502/system/console/topology

Image may be NSFW.
Clik here to view.


3) Access the worker topology management console. It has one instance with sling id da632281-8b9b-4aad-bfe9-89abdff4d471

                            http://nalabotu-osx:4502/system/console/topology

Image may be NSFW.
Clik here to view.


4) In the worker topology, configure sling discovery service with topology connector url (the toplogy connector running on master instance)

                            http://nalabotu-osx:4502/system/console/configMgr/org.apache.sling.discovery.impl.Config

                            Topology Connector URL: http://nalabotu-w7:4502/libs/sling/topology/connector

Image may be NSFW.
Clik here to view.


5) Add the worker hostname and ip address in master discovery service white list configuration.

                            http://nalabotu-w7:4502/system/console/configMgr/org.apache.sling.discovery.impl.Config

                            Topology Connector Whitelist: nalabotu-osx, 192.168.0.8

Image may be NSFW.
Clik here to view.


6) The topology after successfully adding a worker

Image may be NSFW.
Clik here to view.


7) The system should have created necessary replication agents on master. Access the replication agents console on master author. The replication agent responsible for sending asset input payload to worker is named offloading_<worker-sling-id>, and the reverse replication agent for getting processed package back into master is named offloading_reverse_<worker-sling-id>

                            http://nalabotu-w7:4502/etc/replication/agents.author.html


Image may be NSFW.
Clik here to view.


8) Granite offloading console shows the consumers for each topic, when a worker joins topology the consumers for offloading topic com/adobe/granite/workflow/offloading are available and active on both master and worker. To avoid master also processing the uploaded asset (only worker should), disable the consumer on master. In the screenshot below consumer on master (nalabotu-w7 192.168.0.3) is disabled, shown with red button

                            http://nalabotu-w7:4502/libs/granite/offloading/content/view.html

Image may be NSFW.
Clik here to view.


9) The next step is to configure DAM Update Asset Workflow Launchers on Master (nalabotu-w7). This step is to initiate, offloading workflow and not ootb regular update asset. Access Launcher console and change DAM Update Asset to DAM Update Asset Offloading workflow

                            http://nalabotu-w7:4502/libs/cq/workflow/content/console.html


                            Before Update

Image may be NSFW.
Clik here to view.


                            After Updating to DAM Update Asset Offloading

Image may be NSFW.
Clik here to view.


10) Access worker Launcher console and disable the DAM Update Asset Workflow. The assets are not uploaded to worker, but offloaded from master; so DAM Update Asset Workflow should remain disabled on worker

Image may be NSFW.
Clik here to view.


11) Upload any assets to master http://nalabotu-w7:4502/assets.html and the DAM Update Asset process happens on worker http://nalabotu-osx:4502/assets.html. Check the replication agent log

Image may be NSFW.
Clik here to view.


AEM 61 - Touch UI Image Multifield

Goal


Create  a Touch UI Composite Multifield configuration supporting Images, widgets of type granite/ui/components/foundation/form/fileupload

For Classic UI Image Multifieldcheck this post

Hotfix 6670 must be installed for this widget extension to work

Supports Drag & Drop only. Still working on upload piece...

Demo | Package Install


Image Mulitifield

Image may be NSFW.
Clik here to view.


Nodes in CRX

Image may be NSFW.
Clik here to view.


Dialog

Image may be NSFW.
Clik here to view.



Dialog XML

#49 makes the widget a composite multifield by adding empty valued flag eaem-nested
#78 fileReferenceParameter for storing image path

<?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="Touch UI Image Multi Field"
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/tabs"
type="nav"/>
<items jcr:primaryType="nt:unstructured">
<gallery
jcr:primaryType="nt:unstructured"
jcr:title="Gallery"
sling:resourceType="granite/ui/components/foundation/section">
<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="India Dashboard"
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">
<gallery
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Gallery Name"
fieldLabel="Gallery"
name="./gallery"/>
<artist
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/multifield"
class="full-width"
eaem-nested=""
fieldDescription="Click '+' to add a Artist"
fieldLabel="URLs">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fieldset"
name="./artists">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
method="absolute"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<artist
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield"
fieldDescription="Enter Artist Name"
fieldLabel="Artist"
name="./artist"/>
<painting
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/fileupload"
autoStart="{Boolean}false"
class="cq-droptarget"
fieldLabel="Painting"
fileNameParameter="./paintingName"
fileReferenceParameter="./paintingRef"
mimeTypes="[image]"
multiple="{Boolean}false"
name="./painting"
title="Upload Image"
uploadUrl="${suffix.path}"
useHTML5="{Boolean}true"/>
</items>
</column>
</items>
</field>
</artist>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</gallery>
</items>
</content>
</jcr:root>


Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-image-multifield/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

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

                         image-multifield.js

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

(function () {
var DATA_EAEM_NESTED = "data-eaem-nested",
CFFW = ".coral-Form-fieldwrapper",
THUMBNAIL_IMG_CLASS = "cq-FileUpload-thumbnail-img",
SEP_SUFFIX = "-";

function getStringBeforeAtSign(str){
if(_.isEmpty(str)){
return str;
}

if(str.indexOf("@") != -1){
str = str.substring(0, str.indexOf("@"));
}

return str;
}

function getStringAfterAtSign(str){
if(_.isEmpty(str)){
return str;
}

if(str.indexOf("@") != -1){
str = str.substring(str.indexOf("@"));
}else{
str = "";
}

return str;
}

function getStringAfterLastSlash(str){
if(!str || (str.indexOf("/") == -1)){
return "";
}

return str.substr(str.lastIndexOf("/") + 1);
}

/**
* Removes multifield number suffix and returns just the fileRefName
* Input: paintingRef-1, Output: paintingRef
*
* @param fileRefName
* @returns {*}
*/
function getJustFileRefName(fileRefName){
if(!fileRefName || (fileRefName.indexOf(SEP_SUFFIX) == -1)){
return fileRefName;
}

return fileRefName.substring(0, fileRefName.lastIndexOf(SEP_SUFFIX));
}

function getMultiFieldNames($multifields){
var mNames = {}, mName;

$multifields.each(function (i, multifield) {
mName = $(multifield).children("[name$='@Delete']").attr("name");
mName = mName.substring(0, mName.indexOf("@"));
mName = mName.substring(2);
mNames[mName] = $(multifield);
});

return mNames;
}

function buildMultiField(data, $multifield, mName){
if(_.isEmpty(mName) || _.isEmpty(data)){
return;
}

_.each(data, function(value, key){
if(key == "jcr:primaryType"){
return;
}

$multifield.find(".js-coral-Multifield-add").click();

_.each(value, function(fValue, fKey){
if(fKey == "jcr:primaryType"){
return;
}

var $field = $multifield.find("[name='./" + fKey + "']").last();

if(_.isEmpty($field)){
return;
}

$field.val(fValue);
});
});
}

function buildImageField($multifield, mName){
$multifield.find(".coral-FileUpload:last").each(function () {
var $element = $(this), widget = $element.data("fileUpload"),
resourceURL = $element.parents("form.cq-dialog").attr("action"),
counter = $multifield.find(".coral-FileUpload").length;

if (!widget) {
return;
}

var fuf = new Granite.FileUploadField(widget, resourceURL);

addThumbnail(fuf, mName, counter);
});
}

function addThumbnail(imageField, mName, counter){
var $element = imageField.widget.$element,
$thumbnail = $element.find("." + THUMBNAIL_IMG_CLASS),
thumbnailDom;

$thumbnail.empty();

$.ajax({
url: imageField.resourceURL + ".2.json",
cache: false
}).done(handler);

function handler(data){
var fr = getJustFileRefName(getStringAfterLastSlash(imageField.fieldNames.fileReference));

if(_.isEmpty(data[mName]) || _.isEmpty(data[mName][counter])
|| _.isEmpty(data[mName][counter][fr])){
return;
}

var fileRef = data[mName][counter][fr];

if (fileRef) {
if (imageField._isImageMimeType(fileRef)) {
thumbnailDom = imageField._createImageThumbnailDom(fileRef);
} else {
thumbnailDom = $("<p>" + fileRef + "</p>");
}
}

if (!thumbnailDom) {
return;
}

$element.addClass("is-filled");

$thumbnail.append(thumbnailDom);

var $fileRef = $element.find("[name=\"" + imageField.fieldNames.fileReference + "\"]");

$fileRef.val(fileRef);
}
}

//reads multifield data from server, creates the nested composite multifields and fills them
function addDataInFields() {
$(document).on("dialog-ready", function() {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var mNames = getMultiFieldNames($multifields),
$form = $(".cq-dialog"),
actionUrl = $form.attr("action") + ".infinity.json";

$.ajax(actionUrl).done(postProcess);

function postProcess(data){
_.each(mNames, function($multifield, mName){
$multifield.on("click", ".js-coral-Multifield-add", function () {
buildImageField($multifield, mName);
});

buildMultiField(data[mName], $multifield, mName);
});
}
});
}

function collectImageFields($form, $fieldSet, counter){
var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
return $(ele).find(".coral-FileUpload").length == 0;
});

$fields.each(function (j, field) {
var $field = $(field),
$widget = $field.find(".coral-FileUpload").data("fileUpload");

if(!$widget){
return;
}

var $fileRef = $widget.$element.find(".cq-FileUpload-filereference"),
namePath = $fieldSet.data("name") + "/" + (counter + 1) + "/"
+ getJustFileRefName($fileRef.attr("name"));

$('<input />').attr('type', 'hidden')
.attr('name', namePath)
.attr('value', $fileRef.val())
.appendTo($form);

$field.remove();
});
}

function collectNonImageFields($form, $fieldSet, counter){
var $fields = $fieldSet.children().children(CFFW).not(function(index, ele){
return $(ele).find(".coral-FileUpload").length > 0;
});

$fields.each(function (j, field) {
fillValue($form, $fieldSet.data("name"), $(field).find("[name]"), (counter + 1));
});
}

function fillValue($form, fieldSetName, $field, counter){
var name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

//remove the field, so that individual values are not POSTed
$field.remove();

$('<input />').attr('type', 'hidden')
.attr('name', fieldSetName + "/" + counter + "/" + name)
.attr('value', $field.val())
.appendTo($form);
}

//collect data from widgets in multifield and POST them to CRX
function collectDataFromFields(){
$(document).on("click", ".cq-dialog-submit", function () {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var $form = $(this).closest("form.foundation-form"),
$fieldSets, $fields;

$multifields.each(function(i, multifield){
$fieldSets = $(multifield).find("[class='coral-Form-fieldset']");

$fieldSets.each(function (counter, fieldSet) {
collectNonImageFields($form, $(fieldSet), counter);

collectImageFields($form, $(fieldSet), counter);
});
});
});
}

function overrideGranite_computeFieldNames(){
var prototype = Granite.FileUploadField.prototype,
ootbFunc = prototype._computeFieldNames;

prototype._computeFieldNames = function(){
ootbFunc.call(this);

var $imageMulti = this.widget.$element.closest("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($imageMulti)){
return;
}

var fieldNames = {}, counter = $imageMulti.find(".coral-FileUpload").length;

_.each(this.fieldNames, function(value, key){
if(value.indexOf("./jcr:") == 0){
fieldNames[key] = value;
}else{
fieldNames[key] = getStringBeforeAtSign(value) + SEP_SUFFIX
+ counter + getStringAfterAtSign(value);
}
});

this.fieldNames = fieldNames;
}
}

$(document).ready(function () {
addDataInFields();
collectDataFromFields();
});

overrideGranite_computeFieldNames();
})();

5) The sample component in package install, renders images added in composite multifield

Image may be NSFW.
Clik here to view.

AEM 61 - Touch UI Default Image (Placeholder) in Granite File Upload Widget

Goal


Add default (placeholder) image capability to components with Granite File Upload widgets granite/ui/components/foundation/form/fileupload in cq:dialog. In the demo, a custom component /apps/touchui-fileupload-default-image/sample-image-component with two file upload widgets in cq:dialog are configured with placeholder attribute value /content/dam/Paintings/Placeholder.png. When a new component editable is created using drag/drop on page, the file upload widget fileReferenceParameter value is automatically filled with the placeholder file path. When a new component is added, user will always see placeholder image first, before any image can be dropped or uploaded in widget. The placeholder image may provide some instructions on what type of images can be used for the widget (eg. the placeholder may say upload PNGs only)

Demo | Package Install


Placeholder Image added on Component Drop

Image may be NSFW.
Clik here to view.


Placeholder configuration in cq:dialog

Image may be NSFW.
Clik here to view.



Placeholder path in CRX, added on Component Drop

Image may be NSFW.
Clik here to view.



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/touchui-fileupload-default-image

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-fileupload-default-image/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

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

                         default-image.js

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

(function ($document, gAuthor) {
var COMPONENT = "touchui-fileupload-default-image/sample-image-component",
FILE_UPLOAD_WIDGET = "granite/ui/components/foundation/form/fileupload",
DROP_TARGET_ENABLED_CLASS = "js-cq-droptarget--enabled",
FILE_REF_PARAM = "fileReferenceParameter",
PLACEHOLDER_PARAM = "placeholder";

$document.on('cq-inspectable-added', addPlaceholder);

$document.on('mouseup', setNewDropFlag);

var newComponentDrop = false;

function setNewDropFlag(event){
var LM = gAuthor.layerManager;

if (LM.getCurrentLayer() != "Edit") {
return;
}

var DC = gAuthor.ui.dropController,
generalIH = DC._interactionHandler.general;

if (!generalIH._hasStarted || !$(event.target).hasClass(DROP_TARGET_ENABLED_CLASS)) {
return;
}

newComponentDrop = true;
}

function addPlaceholder(event){
var LM = gAuthor.layerManager;

if ( (LM.getCurrentLayer() != "Edit") || !newComponentDrop) {
return;
}

newComponentDrop = false;

var editable = event.inspectable;

if(editable.type !== COMPONENT){
return;
}

$.ajax(editable.config.dialog + ".infinity.json").done(postPlaceholder);

function postPlaceholder(data){
var fileRefs = addFileRefs(data, {});

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

$.ajax({
type : 'POST',
url : editable.path,
data : fileRefs
}).done(function(){
editable.refresh();
})
}

function addFileRefs(data, fileRefs){
if(_.isEmpty(data)){
return fileRefs;
}

_.each(data, function(value, key){
if(_.isObject(value)){
addFileRefs(value, fileRefs);
}

if( (key != "sling:resourceType") || (value != FILE_UPLOAD_WIDGET)){
return;
}

if(!data[FILE_REF_PARAM] || !data[PLACEHOLDER_PARAM]){
return;
}

fileRefs[data[FILE_REF_PARAM]] = data[PLACEHOLDER_PARAM];
}, this);

return fileRefs;
}
}
})($(document), Granite.author);

AEM 61 - Touch UI Assets Console Add Title (dc:title) on File Upload

Goal


Register a fileuploadsuccess listener in Touch UI Assets console to add dc:title metadata with filename - user, after successfully uploading a file (Drag & Drop or Upload button)

Demo | Package Install


Image may be NSFW.
Clik here to view.


Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-asset-drop-listener/clientlib and set a property categories of String type to cq.gui.damadmin.admin

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

                         add-file-name.js

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

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

$(document).on("fileuploadsuccess", "span.coral-FileUpload", addTitle);

function addTitle(event){
try{
var options = event.fileUpload.options,
folderPath = options.uploadUrl.replace(".createasset.html", ""),
assetMetadataPath = folderPath + "/" + event.item.fileName + "/jcr:content/metadata";

var data = {
"dc:title" : event.item.fileName + " - " + getCurrentUser()
};

$.ajax({
type : 'POST',
url : assetMetadataPath,
data : data
}).done(function(){
showAlert(true, data["dc:title"]);
})
}catch(err){
showAlert(false, err.message);
}
}

function showAlert(isSuccessful, data){
var fui = $(window).adaptTo("foundation-ui"), message, options;

if(isSuccessful){
message = "Title added - '" + data + "'";

options = [{
text: "Refresh",
primary: true,
handler: function() {
location.reload();
}
}]
}else{
message = "Error - " + data;

options = [{
text: "OK",
warning: true
}]
}

fui.prompt("Asset Title", message, "notice", options);
}

function getCurrentUser(){
//is there a better way like classic UI? - CQ.User.getCurrentUser()
return $(".endor-Account-caption").html();
}
})(jQuery, $(document));



AEM 61 - Add Watermark to Assets

Goal


Add Watermark to the assets uploaded to AEM 61

Thank you Aanchal Maheshwari for the tip...

Solution


1) Access workflow console http://localhost:4502/libs/cq/workflow/content/console.html

2) Double click on Dam Update Asset workflow http://localhost:4502/cf#/etc/workflow/models/dam/update_asset.html

3) Drag and drop Add Watermark step available in DAM Workflow of Sidekick

Image may be NSFW.
Clik here to view.


4) Double click on the Add Watermark step and fill relevant fields, minimally Text in Arguments tab

 Image may be NSFW.
Clik here to view.


5) Click Ok on the dialog, donot forget to save workflow

6) Access Assets console http://localhost:4502/assets.html/content/dam

7) Upload a sample asset and the watermark should be appear in Position selected Centre, check by accessing asset detail page eg. http://localhost:4502/assetdetails.html/content/dam/geometrixx/Tulips.jpg

 Image may be NSFW.
Clik here to view.

AEM 61 - Sling Resource Merger To Extend Touch UI Component Dialog

Goal


This simple post is on extending Touch UI foundation component cq:dialog to add more input widgets. For details on sling resource mergercheck adobe documentation


Product Image Component Dialog - /libs/foundation/components/image/cq:dialog


Image may be NSFW.
Clik here to view.


Dialog of Image Component Extension - /apps/eaem/image/cq:dialog


Image may be NSFW.
Clik here to view.


Image Component Extension Dialog Structure in CRXDE Lite


Image may be NSFW.
Clik here to view.


Component XML - /apps/eaem/image

<?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"
jcr:primaryType="cq:Component"
jcr:title="EAEM Image"
sling:resourceSuperType="foundation/components/image"
allowedParents="[*/parsys]"
componentGroup="General"/>

Line 5 sling:resourceSuperType="foundation/components/image" makes the component an extension of foundation image component /libs/foundation/components/image


Image Component Extension Dialog As XML - /apps/eaem/image/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="Image"
sling:resourceType="cq/gui/components/authoring/dialog"
helpPath="en/cq/current/wcm/default_components.html#Image">
<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"
margin="{Boolean}false"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<disclaimer
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textarea"
fieldLabel="Disclaimer"
name="./disclaimer"/>
</items>
</column>
</items>
</content>
</jcr:root>

Without copying the entire dialog of foundation image component, using sling resource merger the dialog was extended to add a widget for text disclaimer

AEM 61 - Touch UI Multiple Root Paths in Path Browser Picker

Goal


Support multiple root paths in Path Browser Picker of Touch UI. Path browser /libs/granite/ui/components/foundation/form/pathbrowser, widget configuration supports one root path ootb (can be configured by setting rootPath as shown in demo). This extension is for making the picker support multiple root paths...

Check this post for a similar Tags Picker extension

Demo | Package Install (/apps/touch-ui-pathbrowser-multiple-rootpaths,/apps/experience-aem/sample-component)


Configuration

A sample path browser widget configuration with pickerSrc property set to /apps/touch-ui-pathbrowser-multiple-rootpaths/content/column.html?predicate=hierarchy&eaemRootPaths=/content/dam/geometrixx-outdoors/banners,/content/dam/geometrixx-gov,/content/dam/geometrixx-unlimited/covers


Image may be NSFW.
Clik here to view.



Picker

With pickerSrc having parameter eaemRootPaths set to comma separated values

                     /content/dam/geometrixx-outdoors/banners
                     /content/dam/geometrixx-gov
                     /content/dam/geometrixx-unlimited/covers

the picker shown would be...


Image may be NSFW.
Clik here to view.



Solution


1) Create nt:folder /apps/touch-ui-pathbrowser-multiple-rootpaths, sling:Ordered folder /apps/touch-ui-pathbrowser-multiple-rootpaths/content and nt:unstructured node /apps/touch-ui-pathbrowser-multiple-rootpaths/content/column with sling:resourceType /apps/touch-ui-pathbrowser-multiple-rootpaths/column

2) To render the above content/column node, create nt:folder /apps/touch-ui-pathbrowser-multiple-rootpaths/column and nt:file /apps/touch-ui-pathbrowser-multiple-rootpaths/column/column.jsp, add the following code

<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="org.apache.jackrabbit.util.Text" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%!
String COLUMN_PATH = "/libs/wcm/core/content/common/pathbrowser/column.html";
String NAV_MARKER = "eaemNavMarker";

private String getParentPath(String path) {
return path.substring(0, path.lastIndexOf("/"));
}

private JSONArray getPathsJson(String[] paths){
JSONArray array = new JSONArray();

for(String path: paths){
array.put(path);
}

return array;
}
%>

<%
String rootPathsStr = slingRequest.getParameter("eaemRootPaths");
String[] rootPaths = StringUtils.isEmpty(rootPathsStr) ? new String[0] : rootPathsStr.split(",");

for(String path: rootPaths){
String includePath = COLUMN_PATH + Text.escapePath(getParentPath(path));

%>
<sling:include path="<%=includePath%>" />
<%
}
%>
<div id="<%=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 addRoots(){
var $marker = $("#<%=NAV_MARKER%>"),
$navs = $marker.prevAll("nav"),
paths = <%=getPathsJson(rootPaths)%>,
rootPaths = [];

//find the root paths
$.each(paths, function(index, path){
rootPaths.push($navs.find("[data-value='" + path + "']"));
});

removeAddnNavsGetColumn($navs).append(rootPaths);

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

addRoots();
}(jQuery));
</script>

3) The picker column renderer created above includes the ootb column renderer /libs/wcm/core/content/common/pathbrowser/column.html for parents of each root path (#33), does some simple dom manipulation on the client side to remove unwanted node html

4) The extension structure in CRXDE Lite http://localhost:4502/crx/de/index.jsp

Image may be NSFW.
Clik here to view.

AEM 6 SP2 - Reset Administrator Password to admin offline

Goal


The default password for administrator user admin is admin. If the password was changed to something else, to reset it to admin without logging in...

Thanks to Yuval Ararat for the tip

Solution


1) Stop AEM 6 instance

2) Place the package reset_admin_pass_to_admin.zip in <AEM_Home>/crx-quickstart/install (create it, if there isn't one)

3) Start AEM 6 instance

4) Package should have gotten installed as shown below. It contains /home/users/a/admin/.content.xml with rep:password set to text admin encrypted 

Image may be NSFW.
Clik here to view.


5) Login as admin/admin

AEM 61 - Touch UI Composite Multifield with Rich Text Editor (RTE)

Goal


Create a composite multifield comprised of rich text editors (widgets of type  cq/gui/components/authoring/dialog/richtext)

Demo | Package Install


Image may be NSFW.
Clik here to view.


Solution


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

2) Create clientlib (type cq:ClientLibraryFolder/apps/touchui-rte-multifield/clientlib and set a property categories of String type to cq.authoring.dialogdependencies of type String[] with value underscore

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

                         rte-multifield.js

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

(function () {
var DATA_EAEM_NESTED = "data-eaem-nested";
var CFFW = ".coral-Form-fieldwrapper";
var RTE_CONTAINER = "richtext-container";

function setSelect($field, value){
var select = $field.closest(".coral-Select").data("select");

if(select){
select.setValue(value);
}
}

function setHiddenOrRichText($field, value){
$field.val(value);

var $parent = $field.parent();

if(!$parent.hasClass(RTE_CONTAINER)){
return;
}

$field.next(".editable").empty().append(value);
}

function setCheckBox($field, value){
$field.prop( "checked", $field.attr("value") == value);
}

//reads multifield data from server, creates the nested composite multifields and fills them
function addDataInFields() {
function getMultiFieldNames($multifields){
var mNames = {}, mName;

$multifields.each(function (i, multifield) {
mName = $(multifield).children("[name$='@Delete']").attr("name");

mName = mName.substring(0, mName.indexOf("@"));

mName = mName.substring(2);

mNames[mName] = $(multifield);
});

return mNames;
}

function buildMultiField(data, $multifield, mName){
if(_.isEmpty(mName) || _.isEmpty(data)){
return;
}

_.each(data, function(value, key){
if(key == "jcr:primaryType"){
return;
}

$multifield.find(".js-coral-Multifield-add").click();

_.each(value, function(fValue, fKey){
if(fKey == "jcr:primaryType"){
return;
}

var $field = $multifield.find("[name='./" + fKey + "']").last(),
type = $field.prop("type");

if(_.isEmpty($field)){
return;
}

//handle single selection dropdown
if(type == "select-one"){
setSelect($field, fValue);
}else if(type == "checkbox"){
setCheckBox($field, fValue);
}else if(type == "hidden"){
setHiddenOrRichText($field, fValue);
}else{
$field.val(fValue);
}
});
});
}

$(document).on("dialog-ready", function() {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var mNames = getMultiFieldNames($multifields),
$form = $(".cq-dialog"),
actionUrl = $form.attr("action") + ".infinity.json";

$.ajax(actionUrl).done(postProcess);

function postProcess(data){
_.each(mNames, function($multifield, mName){
buildMultiField(data[mName], $multifield, mName);
});
}
});
}

//collect data from widgets in multifield and POST them to CRX
function collectDataFromFields(){
function fillValue($form, fieldSetName, $field, counter){
var name = $field.attr("name");

if (!name) {
return;
}

//strip ./
if (name.indexOf("./") == 0) {
name = name.substring(2);
}

var value = $field.val();

if( $field.prop("type") == "checkbox" ){
value = $field.prop("checked") ? $field.val() : "";
}

$('<input />').attr('type', 'hidden')
.attr('name', fieldSetName + "/" + counter + "/" + name)
.attr('value', value )
.appendTo($form);

//remove the field, so that individual values are not POSTed
$field.remove();
}

$(document).on("click", ".cq-dialog-submit", function () {
var $multifields = $("[" + DATA_EAEM_NESTED + "]");

if(_.isEmpty($multifields)){
return;
}

var $form = $(this).closest("form.foundation-form"),
$fieldSets, $fields;

$multifields.each(function(i, multifield){
$fieldSets = $(multifield).find("[class='coral-Form-fieldset']");

$fieldSets.each(function (counter, fieldSet) {
$fields = $(fieldSet).children().children(CFFW);

$fields.each(function (j, field) {
fillValue($form, $(fieldSet).data("name"), $(field).find("[name]"), (counter + 1));
});
});
});
});
}

$(document).ready(function () {
addDataInFields();
collectDataFromFields();
});
})();

AEM 61 - Get References of a Page or Asset

Goal


Servlet for returning Page or Asset references....

Solution


1) Install OSGI Servlet apps.experienceaem.references.GetReferences with the following code

package apps.experienceaem.references;

import com.day.cq.commons.TidyJSONWriter;
import com.day.cq.wcm.commons.ReferenceSearch;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;

import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Collection;

@SlingServlet
@Properties({
@Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
@Property(name = "sling.servlet.paths", value = "/bin/experience-aem/references", propertyPrivate = true),
@Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)})
public class GetReferences extends SlingAllMethodsServlet {
@Override
protected void doGet(SlingHttpServletRequest request,SlingHttpServletResponse response)
throws ServletException, IOException {
String path = request.getParameter("path");

if(StringUtils.isEmpty(path)){
throw new ServletException("Empty path");
}

try{
ResourceResolver resolver = request.getResourceResolver();
TidyJSONWriter writer = new TidyJSONWriter(response.getWriter());

ReferenceSearch referenceSearch = new ReferenceSearch();
referenceSearch.setExact(true);
referenceSearch.setHollow(true);
referenceSearch.setMaxReferencesPerPage(-1);

Collection<ReferenceSearch.Info> resultSet = referenceSearch.search(resolver, path).values();

writer.array();

for (ReferenceSearch.Info info: resultSet) {
for (String p: info.getProperties()) {
writer.value(p);
}
}

writer.endArray();
}catch(Exception e){
throw new ServletException("Error getting references", e);
}
}
}

2) Sample call returning asset referenceshttp://localhost:4502/bin/experience-aem/references?path=/content/dam/geometrixx/portraits/jane_doe.jpg

Image may be NSFW.
Clik here to view.

3) Sample call returning page references - http://localhost:4502/bin/experience-aem/references?path=/content/geometrixx/en

Image may be NSFW.
Clik here to view.



AEM 61 - Extend Classic UI Rich Text Editor Table Plugin, Add Summary Field

Goal


Extend Classic UI Rich Text Editor (RTE) widget to add a field for Table Summary

Demo | Package Install


Table Properties Dialog with Summary


Image may be NSFW.
Clik here to view.



Table Source with Summary


 Image may be NSFW.
Clik here to view.



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/classicui-rte-table-summary

2) Create clientlib (type cq:ClientLibraryFolder/apps/classicui-rte-table-summary/clientlib and set a property categories of String type to cq.widgets

3) Create file ( type nt:file ) /apps/classicui-rte-table-summary/clientlib/js.txt, add the following

                         summary.js

4) Create file (type nt:file) /apps/rte-pull-quote/clientlib/summary.js, add the following code

CQ.Ext.ns("ExperienceAEM");

ExperienceAEM.ToolkitImpl = new Class({
toString: "EAEMToolkitImpl",

extend: CUI.rte.ui.ext.ToolkitImpl,

//extend the dialog manager
createDialogManager: function(editorKernel) {
return new ExperienceAEM.ExtDialogManager(editorKernel);
}
});

CUI.rte.ui.ToolkitRegistry.register("ext", ExperienceAEM.ToolkitImpl);

ExperienceAEM.ExtDialogManager = new Class({
toString: "EAEMExtDialogManager",

extend: CUI.rte.ui.ext.ExtDialogManager,

//add the summary widget to table properties dialog
createTablePropsDialog: function(cfg) {
var dialog = this.superClass.createTablePropsDialog.call(this, cfg);

var fields = dialog.findByType("form")[0];

fields.add({
"itemId": "summary",
"name": "summary",
"xtype": "textarea",
"width": 170,
"fieldLabel": "Summary"
});

dialog.setHeight(400);

return dialog;
}
});

ExperienceAEM.Table = new Class({
toString: "EAEMTable",

extend: CUI.rte.commands.Table,

//add/remove the summary
transferConfigToTable: function(dom, execDef) {
this.superClass.transferConfigToTable.call(this, dom, execDef);

var com = CUI.rte.Common,
config = execDef.value;

if (config.summary) {
com.setAttribute(dom, "summary", config.summary);
} else {
com.removeAttribute(dom, "summary");
}
}
});

CUI.rte.commands.CommandRegistry.register("_table", ExperienceAEM.Table);

AEM 61 - Disable SideKick, SiteAdmin Page Properties OK button for specified milliseconds

Goal


Get the OK button of Page Properties Dialog in SiteAdmin, SideKick and disable it few millseconds...

Demo | Package Install


SideKick

 Image may be NSFW.
Clik here to view.


SiteAdmin

 Image may be NSFW.
Clik here to view.



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/classic-ui-delay-page-properties-ok

2 Create node /apps/classic-ui-delay-page-properties-ok/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/classic-ui-delay-page-properties-ok/clientlib/js.txt and add

                      delay-ok.js

4) Create file (nt:file) /apps/classic-ui-delay-page-properties-ok/clientlib/delay-ok.js and add the following code.

(function(){
var DELAY = 5000, //millis
PROPS_DIALOG_PREFIX = "cq-propsdialog-",
SA_GRID = "cq-siteadmin-grid";

//for sidekick page properties dialog
if( window.location.pathname.indexOf("/content") == 0 ){
handleSideKickPPDialog();
}

//for siteadmin page properties dialog
if( window.location.pathname.indexOf("/siteadmin") == 0 ){
handleSiteAdminPPDialog();
}

function handleSideKickPPDialog(){
var SK_INTERVAL = setInterval(function(){
var sk = CQ.WCM.getSidekick();

if(sk){
clearInterval(SK_INTERVAL);

disableSKOk(sk);
}
}, 250);

function disableSKOk(sk){
var dialog = CQ.WCM.getDialog(sk.initialConfig.propsDialog);

if(!dialog){
return;
}

var OK_TIMEOUT;

dialog.on('show', function(){
var okButton = dialog.buttons[0];

okButton.setDisabled(true);

OK_TIMEOUT = setTimeout(function(){
okButton.setDisabled(false);
}, DELAY);
});

dialog.on('hide', function(){
//sidekick page properties dialog is not destroyed (unlike siteadmin page properties)
//so clear timeout if the dialog was cancelled before timeout
clearTimeout(OK_TIMEOUT);
})
}
}

function handleSiteAdminPPDialog(){
function disableSiteAdminOk(){
try{
//find the extjs dialog component by querying underlying dom structure
var element = CQ.Ext.get($("[id^=cq-propsdialog-]")[0].id),
tabChild = element.child('div.x-tab-panel'),
tabPanel = CQ.Ext.getCmp(tabChild.id),
dialog = tabPanel.findParentByType("dialog");

var okButton = dialog.buttons[0];

okButton.setDisabled(true);

setTimeout(function(){
//if the dialog was cancelled before timeout, ok button gets destroyed
if(okButton.el.dom){
okButton.setDisabled(false);
}
}, DELAY);
}catch(err){
console.log("Error getting properties dialog", err);
}
}

function addListener(){
var DG_INTERVAL = setInterval(function(){
var propsDiv = $("[id^=" + PROPS_DIALOG_PREFIX + "]");

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

clearInterval(DG_INTERVAL);

disableSiteAdminOk();
}, 250);
}

function addPropertiesListener(grid){
grid.on('rowcontextmenu',function(grid){
var properties = grid.contextMenu.find("text", "Properties...");

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

//add the disable listeners on properties button click
properties[0].on('click', addListener());
}, this);
}

var INTERVAL = setInterval(function(){
var grid = CQ.Ext.getCmp(SA_GRID);

if(grid){
clearInterval(INTERVAL);

addPropertiesListener(grid);
}
}, 250);
}
})();

AEM 61 - Publishing MCM Campaign Experiences With Page Publish in Workflow Step

Goal


Publish MCM (Marketing Campaign Manager) Campaign, Adobe Target Experiences with Page Publish in a Workflow. When author activates page using Sidekick Activate Page button, any references, including campaigns are shown for selection; author can optionally select campaigns to be published with page. If the page is published in workflow step, any associated campaigns, target experiences are not published automatically. This post is on adding a Process Step in workflow to publish the referenced Campaigns and associated Adobe Target Experiences

Most of the code pieces were collected from various sources. Thanks to the unknown coders

Demo | Package Install | Source Code


Campaign References Shown with Activate Page Click


Image may be NSFW.
Clik here to view.



Solution


1) Create an OSGI service apps.experienceaem.campaign.PublishCampaignStep extending com.day.cq.workflow.exec.WorkflowProcess, add the following code...

package apps.experienceaem.campaign;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationOptions;
import com.day.cq.replication.ReplicationStatus;
import com.day.cq.replication.Replicator;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.reference.*;
import com.day.cq.wcm.api.reference.Reference;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.HistoryItem;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.metadata.MetaDataMap;
import com.day.cq.workflow.model.WorkflowNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.resource.JcrResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.apache.felix.scr.annotations.ReferenceCardinality.OPTIONAL_MULTIPLE;
import static org.apache.felix.scr.annotations.ReferencePolicy.DYNAMIC;

@Component
@Service(WorkflowProcess.class)
@Property(name = "process.label", value = "Experience AEM Publish Campaign for Page")
public class PublishCampaignStep implements WorkflowProcess {
private final Logger log = LoggerFactory.getLogger(PublishCampaignStep.class);

private static final String REPLICATE_AS_PARTICIPANT = "replicateAsParticipant";
private static final String PROCESS_ARGS = "PROCESS_ARGS";

private static final String PN_OFFERPATH = "offerPath";
private static final String REF_CAMPAIGN = "campaign";
private static final String LOCATION = "location";


private static final String ARP = "com.day.cq.dam.commons.util.impl.AssetReferenceProvider";

@org.apache.felix.scr.annotations.Reference(policy = ReferencePolicy.STATIC)
private JcrResourceResolverFactory factory;

@org.apache.felix.scr.annotations.Reference
private Replicator replicator;

@org.apache.felix.scr.annotations.Reference(
referenceInterface = ReferenceProvider.class,
cardinality = OPTIONAL_MULTIPLE,
policy = DYNAMIC)
private final List<ReferenceProvider> referenceProviders = new CopyOnWriteArrayList<ReferenceProvider>();

protected void bindReferenceProviders(ReferenceProvider referenceProvider) {
referenceProviders.add(referenceProvider);
}

protected void unbindReferenceProviders(ReferenceProvider referenceProvider) {
referenceProviders.remove(referenceProvider);
}

@Override
public void execute(WorkItem item, WorkflowSession session, MetaDataMap metaData)
throws WorkflowException {
try{
Session userSession = session.getSession();
ResourceResolver resolver = factory.getResourceResolver(userSession);

Resource page = getResourceFromPayload(resolver, item, session.getSession());
Resource jcrContent = resolver.getResource(page.getPath() + "/" + JcrConstants.JCR_CONTENT);

Set<Reference> allReferences = new TreeSet<Reference>(new Comparator<Reference>() {
public int compare(Reference o1, Reference o2) {
return o1.getResource().getPath().compareTo(o2.getResource().getPath());
}
});

for (ReferenceProvider referenceProvider : referenceProviders) {
allReferences.addAll(referenceProvider.findReferences(jcrContent));
}

Session participantSession = null;

if (replicateAsParticipant(metaData)) {
String approverId = resolveParticipantId(item, session);

if (StringUtils.isNotEmpty(approverId)) {
participantSession = getParticipantSession(approverId, session);
}
}

List<String> toPublish = getNotPublishedOrOutdatedReferences(page, allReferences, userSession);

publishReferences(toPublish, (participantSession != null) ? participantSession : userSession);
}catch(Exception e){
throw new WorkflowException("Error publishing campaign", e);
}
}

private Session getParticipantSession(String participantId, WorkflowSession session) {
try {
return session.getSession().impersonate(
new SimpleCredentials(participantId, new char[0]));
} catch (Exception e) {
log.warn(e.getMessage());
return null;
}
}

/**
* See the session's history to find latest participant step or dynamic participant step and use it's current assignee
* @param workItem
* @param session
* @return
*/
private String resolveParticipantId(WorkItem workItem, WorkflowSession session) {
List<HistoryItem> history = new ArrayList<HistoryItem>();

try {
history = session.getHistory(workItem.getWorkflow());

for (int index = history.size() - 1; index >= 0; index--) {
HistoryItem previous = history.get(index);
String type = previous.getWorkItem().getNode().getType();

if (type != null && (type.equals(WorkflowNode.TYPE_PARTICIPANT)
|| type.equals(WorkflowNode.TYPE_DYNAMIC_PARTICIPANT))) {
return previous.getUserId();
}
}
} catch (Exception e) {
log.warn("Error getting participant id", e);
}

return null;
}

private boolean replicateAsParticipant(MetaDataMap args) {
String processArgs = args.get(PROCESS_ARGS, String.class);

if(StringUtils.isEmpty(processArgs)){
return false;
}

String[] arguments = processArgs.split(",");

for (String argument : arguments) {
String[] split = argument.split("=");

if (split.length == 2) {
if (split[0].equalsIgnoreCase(REPLICATE_AS_PARTICIPANT)) {
return Boolean.parseBoolean(split[1]);
}
}
}

return false;
}

private List<String> getNotPublishedOrOutdatedReferences(Resource page, Set<Reference> allReferences,
Session session){
Resource resource = null;
boolean canReplicate = false;

List<String> toPublish = new ArrayList<String>();

for (Reference reference : allReferences) {
resource = reference.getResource();

if (resource == null) {
continue;
}

canReplicate = canReplicate(resource.getPath(), session);

if(!canReplicate){
log.warn("Skipping, No replicate permission on - " + resource.getPath());
continue;
}

if(shouldReplicate(reference)){
toPublish.add(resource.getPath());
}

if(reference.getType().equals(REF_CAMPAIGN)){
collectCampaignReferences(resource.adaptTo(Page.class), toPublish, session, page);
}
}

log.info("Publishing campaign assets - " + toPublish);

return toPublish;
}

private boolean shouldReplicate(Reference reference){
Resource resource = reference.getResource();
ReplicationStatus replStatus = resource.adaptTo(ReplicationStatus.class);

if (replStatus == null) {
return true;
}

boolean doReplicate = false, published = false, outdated = false;
long lastPublished = 0;

published = replStatus.isDelivered() || replStatus.isActivated();

if (published) {
lastPublished = replStatus.getLastPublished().getTimeInMillis();
outdated = lastPublished < reference.getLastModified();
}

if (!published || outdated) {
doReplicate = true;
}

return doReplicate;
}

private void publishReferences(List<String> toPublish, Session session) throws Exception{
ReplicationOptions opts = new ReplicationOptions();

for(String path : toPublish){
replicator.replicate(session, ReplicationActionType.ACTIVATE, path, opts);
}
}

private void collectCampaignReferences(Page root, List<String> toPublish, Session session,
Resource payloadPage) {
ReferenceProvider assetReferenceProvider = null;

for (ReferenceProvider referenceProvider : referenceProviders) {
if(!(referenceProvider.getClass().getName().equals(ARP))){
continue;
}
assetReferenceProvider = referenceProvider;
}

if(assetReferenceProvider == null){
return;
}

Iterator<Page> experiences = root.listChildren();
Page experience, offer; String assetPath;
boolean canReplicate = false; String location;

Resource contentRes = null;
List<Reference> assetRefs = null;

while(experiences.hasNext()) {
experience = experiences.next();

Iterator<Page> offers = experience.listChildren();

while (offers.hasNext()) {
offer = offers.next();

contentRes = offer.getContentResource();

location = contentRes.adaptTo(ValueMap.class).get(LOCATION, "");

if(!location.startsWith(payloadPage.getPath() + "/jcr:content")){
log.debug("Publish campaign step skipping - " + offer.getPath());
continue;
}

toPublish.add(experience.getPath());

toPublish.add(offer.getPath());

ValueMap properties = offer.getProperties();
String offerPath = properties.get(PN_OFFERPATH,"");

if (StringUtils.isNotEmpty(offerPath)) {
toPublish.add(offerPath);
}

assetRefs = assetReferenceProvider.findReferences(contentRes);

for(Reference ref : assetRefs){
assetPath = ref.getResource().getPath();
canReplicate = canReplicate(assetPath, session);

/*if(!canReplicate){
log.warn("Skipping, No replicate permission on - " + assetPath);
continue;
}

if(!shouldReplicate(ref)){
continue;
}*/

toPublish.add(assetPath);
}
}
}
}

private static boolean canReplicate(String path, Session session) {
try {
AccessControlManager acMgr = session.getAccessControlManager();

return acMgr.hasPrivileges(path, new Privilege[]{
acMgr.privilegeFromName(Replicator.REPLICATE_PRIVILEGE)
});
} catch (RepositoryException e) {
return false;
}
}

private Resource getResourceFromPayload(ResourceResolver resolver, WorkItem item,
Session session) {
if (!item.getWorkflowData().getPayloadType().equals("JCR_PATH")) {
return null;
}

String path = item.getWorkflowData().getPayload().toString();

return resolver.getResource(path);
}
}


2) #240 collectCampaignReferences() method, adds the experiences of campaign to list of paths to be published collection

3) Process Step added in workflow Publish Campaign Example (copy of Publish Example)

Image may be NSFW.
Clik here to view.



AEM 60 - Replicate Assets from 60 Author Instance to 61 Author Instance

Goal


Replicate Assets from 60 author to 61 author using workflows (ideally any author-author)

Demo | Package Install (Includes Replication Agent and Workflow)


Solution


1) Create a Replication Agent on 60 author connecting to 61 author. Assuming the source author 60 is running on http://localhost:4502/ and destination author 61 running on http://localhost:5502/....

           a. Access http://localhost:4502/miscadmin#/etc/replication/agents.author

           b. Click New, select Replication Agent

                         Title: Replicate to 61 Author
                         Name: 61_author (this name is used in AgentFilter of ReplicationStep, added in next steps)


Image may be NSFW.
Clik here to view.

         
c. Agent as xml - /etc/replication/agents.author/61_author

<?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="cq:Page">
<jcr:content
cq:lastModified="{Date}2015-08-13T11:51:39.314-05:00"
cq:lastModifiedBy="admin"
cq:template="/libs/cq/replication/templates/agent"
jcr:lastModified="{Date}2015-08-13T11:51:39.286-05:00"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="Replicate to 61 Author"
sling:resourceType="cq/replication/components/agent"
enabled="true"
protocolHTTPMethod="POST"
serializationType="durbo"
ssl="default"
transportPassword="\{2878481d659c461d12b8db7a89ee0c422749e5f2958a757d00a79819da0f0187}"
transportUri="http://localhost:5502/bin/receive?sling:authRequestLogin=1"
transportUser="admin"
triggerSpecific="true"/>
</jcr:root>

2) Create a Process Step apps.experienceaem.replication.ReplicationStep used for replicating the asset, with following code

package apps.experienceaem.replication;

import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.process.AbstractAssetWorkflowProcess;
import com.day.cq.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.metadata.MetaDataMap;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import java.util.ArrayList;
import java.util.List;

@Component
@Service
@Properties({@Property(name = "process.label", value = "Replicate Assets to 61 Author")})
public class ReplicationStep extends AbstractAssetWorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(ReplicationStep.class);

// the agent used for replication
private static final String REP_AGENT_61 = "61_author";

@Reference
Replicator replicator;

@Reference
private AgentManager agentMgr = null;

@Reference
ResourceResolverFactory resourceResolverFactory;

@Override
public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
throws WorkflowException {
try{
if(!agentExists()){
log.info("NO Replication agent " + REP_AGENT_61 + ", skipping replicating to 61");
return;
}

Session session = workflowSession.getSession();
Asset asset = getAssetFromPayload(workItem, session);

List<String> paths = new ArrayList<String>();

//add the file path to replication queue
paths.add(asset.getPath());

doReplicate(paths, session);
}catch(Exception e){
throw new WorkflowException("Error replicating to 61 Author", e);
}
}

private boolean agentExists(){
return agentMgr.getAgents().containsKey(REP_AGENT_61);
}

private void doReplicate(List<String> paths, Session session) throws Exception{
ReplicationOptions opts = new ReplicationOptions();

//use the 61 replication agent
opts.setFilter(new AgentFilter() {
public boolean isIncluded(com.day.cq.replication.Agent agent) {
return agent.getId().equalsIgnoreCase(REP_AGENT_61);
}
});

for(String path : paths){
replicator.replicate(session, ReplicationActionType.ACTIVATE, path, opts);
}
}

public ResourceResolver getResourceResolver(final Session session) {
ResourceResolver resourceResolver = null;

try {
resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
} catch (LoginException e) {
log.error("Error obtaining the resourceresolver",e);
}

return resourceResolver;
}
}


3)  Create a workflow Replicate to 61 Author - /etc/workflow/models/replicate-to-61-author


Image may be NSFW.
Clik here to view.


4) Select an asset and start workflow Replicate to 61 Author

Image may be NSFW.
Clik here to view.

AEM 61 - Packaging Tips

1) To exclude thumbnails when creating a package of images available in some DAM folder...

     With exclude rule .*/cq5dam.* in the following filter, thumbnails created by DAM Update Asset workflow (cq5dam.thumbnail.48.48.png, cq5dam.thumbnail.48.48.png, cq5dam.thumbnail.319.319.png) are not added to package created

Image may be NSFW.
Clik here to view.


 2) 
Viewing all 526 articles
Browse latest View live


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