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

AEM 6510 - Adding Custom Metadata Columns in List View of Asset Search Console

$
0
0

Goal


Add custom metadata columns in Assets Search console - http://localhost:4502/aem/search.html

Demo | Package Install | Github


Custom Columns in List View



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-metadata-columns-in-search-results

2) Add and configure the columns folder /apps/eaem-metadata-columns-in-search-results/columns, for example..



3) Create node /apps/eaem-metadata-columns-in-search-results/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.common.admin.searchpanel], String[] property dependencies with value lodash.

4) Create file (nt:file) /apps/eaem-metadata-columns-in-search-results/clientlib/js.txt, add

                        metadata-columns.js

5) Create file (nt:file) /apps/eaem-metadata-columns-in-search-results/clientlib/metadata-columns.js, add the following code

(function ($, $document) {
var FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
GRANITE_OMNI_SEARCH_RESULT = "#granite-omnisearch-result",
EAEM_METADATA_REL_PATH = "data-eaem-metadata-rel-path",
ROW_SELECTOR = "tr.foundation-collection-item",
GRANITE_OMNI_SEARCH_CONTENT = ".granite-omnisearch-content",
EAEM_META_COLUMNS_URL = "/apps/eaem-metadata-columns-in-search-results/columns.1.json",
metaParams = {}, results = {};

$document.on(FOUNDATION_CONTENT_LOADED, GRANITE_OMNI_SEARCH_CONTENT, function(event){
_.defer(function(){
handleContentLoad(event);
});
});

loadCustomColumnHeaders();

function handleContentLoad(event){
var layout = $(GRANITE_OMNI_SEARCH_RESULT).data("foundationLayout");

if(!layout || (layout.layoutId !== "list")){
return;
}

addColumnHeaders();

fillColumnData(results);
}

function addColumnHeaders(){
if(checkIFHeadersAdded()){
return;
}

var $fui = $(window).adaptTo("foundation-ui");

$fui.wait();

var $container = $(GRANITE_OMNI_SEARCH_CONTENT),
$headRow = $container.find("thead > tr");

_.each(metaParams, function(header, metaPath){
$headRow.append(getTableHeader(header, metaPath));
});

$fui.clearWait();
}

function addOnFormSubmitListener() {
var $form = $("form.foundation-form");

$form.on("foundation-form-submitted", handler);

function handler(event, success, xhr){
if (!success) {
return;
}

var query = "/bin/querybuilder.json?" + $(this).serialize();

query = query + "&999_property=jcr:primaryType&999_property.value=dam:Asset&p.hits=selective&p.limit=-1&p.properties=jcr:path";

query = query + "+" + Object.keys(metaParams).join("+");

$.ajax({ url: query, async: false }).done(handleResults);
}
}

function handleResults(data){
if(!data || (data.results <= 0) ){
return;
}
var $fui = $(window).adaptTo("foundation-ui");

$fui.wait();

results = {};

_.each(data.hits, function(hit){
results[hit["jcr:path"]] = hit["jcr:content"]["metadata"];
});

$fui.clearWait();
}

function fillColumnData(results){
if(_.isEmpty(results)){
return;
}

var $fui = $(window).adaptTo("foundation-ui");

$fui.wait();

$(ROW_SELECTOR).each(function(index, item){
itemHandler($(item) );
});

function itemHandler($row){
if(!_.isEmpty($row.find("[" + EAEM_METADATA_REL_PATH + "]"))){
return;
}

if(_.isEmpty($row.find("td.foundation-collection-item-title"))){
return;
}

var itemPath = $row.data("foundation-collection-item-id"),
metadata, metaProp, $td = $row.find("td:last");

_.each(metaParams, function(header, metaPath){
metadata = (results[itemPath] || {});

metaProp = metaPath.substring(metaPath.lastIndexOf("/") + 1);

$td = $(getListCellHtml(metaPath, metadata[metaProp])).insertAfter($td);
});
}

$fui.clearWait();
}

function getListCellHtml(metaPath, metaValue){
metaValue = (metaValue || "");

return '<td is="coral-table-cell"' + EAEM_METADATA_REL_PATH + '="' + metaPath + '">' + metaValue + '</td>';
}

function loadCustomColumnHeaders(){
addOnFormSubmitListener();

$.ajax( { url: EAEM_META_COLUMNS_URL, async: false} ).done(function(data){
_.each(data, function(colData){
if(_.isEmpty(colData.header) || _.isEmpty(colData.metadataPath)){
return;
}

metaParams[colData.metadataPath] = colData.header;
});
});
}

function getTableHeader(colText, metadataPath) {
return '<th is="coral-table-headercell"' + EAEM_METADATA_REL_PATH + '="' + metadataPath + '">' + colText + '</th>';
}

function checkIFHeadersAdded(){
return !_.isEmpty($(GRANITE_OMNI_SEARCH_CONTENT).find("thead > tr").find("[" + EAEM_METADATA_REL_PATH + "]"));
}
})(jQuery, jQuery(document));


AEM 6510 - Multi Site Manager Show Component Last Modified during Rollout

$
0
0

Goal


In AEM product, when an author clicks on Component Rollout to... menu item, the Live Copies console shows Page Modified information, this post is on extending the console to show Component modified (or component last rolled out)

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-msm-show-component-rollout-column

2) Create node /apps/eaem-msm-show-component-rollout-column/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.editor.sites.page], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-msm-show-component-rollout-column/clientlib/js.txt, add

                        rollout-column.js

4) Create file (nt:file) /apps/eaem-msm-show-component-rollout-column/clientlib/rollout-column.js, add the following code

(function ($, $document) {
var EDITOR_LOADED_EVENT = "cq-editor-loaded",
QUERY = "/bin/querybuilder.json?type=nt:unstructured&path=/content&p.limit=-1&nodename=";

$document.on(EDITOR_LOADED_EVENT, extendMSMOpenDialog);

function extendMSMOpenDialog(){
if(!Granite.author || !Granite.author.MsmAuthoringHelper){
console.log("Experience AEM - Granite.author.MsmAuthoringHelper not available");
return;
}

var _origFn = Granite.author.MsmAuthoringHelper.openRolloutDialog;

Granite.author.MsmAuthoringHelper.openRolloutDialog = function(dialogSource){
_origFn.call(this, dialogSource);

var dialog = Granite.author.DialogFrame.currentDialog,
_onReady = dialog.onReady;

dialog.onReady = function(){
_onReady.call(this);

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

getComponentRolloutData();
}
}
}

function getComponentRolloutData(){
var compName = getSelectedComponentPath();

compName = compName.substring(compName.lastIndexOf("/") + 1);

$.ajax(QUERY + compName).done(addComponentDataColumn);
}

function addComponentDataColumn(compData){
if(!compData || (compData.results <= 0) ){
return;
}

var results = {};

_.each(compData.hits, function(hit){
results[hit["path"]] = hit["lastModified"];
});

var $rolloutDialog = $(".msm-rollout-dialog"),
$modifiedDiv = $rolloutDialog.find("header").find(".modified"),
$componentDiv = $($modifiedDiv[0].outerHTML).html("Component Modified").css("width","20%");

$modifiedDiv.html("Page Modified").css("width","20%").before($componentDiv);

var compPath = getSelectedComponentPath(),
$articles = $rolloutDialog.find(".live-copy-list-items article");

compPath = compPath.substring(compPath.indexOf("/jcr:content"));

$articles.each(function(index, article){
var $article = $(article),
$pageMod = $article.find(".modified"),
lastModified = results[$article.data("path") + compPath];

if(!lastModified){
return;
}

lastModified = new Date(lastModified);

var $componentMod = $($pageMod[0].outerHTML).css("width","20%"),
$dateField = $componentMod.attr("title", "Component Modification Data").find(".date");

$dateField.html(lastModified.toDateString()).attr("data-timestamp", lastModified.getMilliseconds());

$pageMod.css("width","20%").before($componentMod);
})
}

function getSelectedComponentPath(){
var selEditables = MSM.MSMCommons.getSelection(),
selComps = [];

$.each(selEditables, function(index, editable) {
selComps.push(editable.path);
});

return ( (selComps.length == 1) ? selComps[0] : "");
}
}(jQuery, jQuery(document)));

AEM 6510 - Show Filename and not Title in Asset Details and Properties Page

$
0
0

Goal


Show file name (with extension) and not title (dc:title) in Properties and Details page

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-show-file-name-in-asset-properties

2) Create node /apps/eaem-show-file-name-in-asset-properties/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [dam.gui.admin.coral], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-show-file-name-in-asset-properties/clientlib/js.txt, add

                        show-file-name.js

4) Create file (nt:file) /apps/eaem-show-file-name-in-asset-properties/clientlib/show-file-name.js, add the following code

(function ($, $document) {
$document.on("foundation-contentloaded", showFileName);

function showFileName(){
var fileName = window.Dam.Util.getUrlParam("item");

if(isAssetDetailsPage()){
fileName = window.location.pathname;
}

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

var $title = $(".granite-title");

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

fileName = fileName.substring(fileName.lastIndexOf("/") + 1);

$title.find("[role=heading]").html(fileName);
}

function isAssetDetailsPage(){
return window.location.pathname.startsWith("/assetdetails.html");
}
}(jQuery, jQuery(document)));

AEM 6510 - Show full Filename and not Ellipsis in Autocomplete Asset Picker

$
0
0

Goal


AEM's Autocomplete Asset picker shows Ellipsis at the end, when Full filename does not fit in column view. This extension is for extending the picker and show full filename by wrapping it....

For Tags check this post

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-show-full-file-name-in-picker

2) Create node /apps/eaem-show-full-file-name-in-picker/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [granite.ui.coral.foundation], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-show-full-file-name-in-picker/clientlib/js.txt, add

                        show-full-file-name.js

4) Create file (nt:file) /apps/eaem-show-full-file-name-in-picker/clientlib/show-full-file-name.js, add the following code

(function ($, $document) {
$document.on("foundation-contentloaded", handleAssetPicker);

function handleAssetPicker(){
var $autoCompletes = $("foundation-autocomplete");

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

_.each($autoCompletes, function(autoComplete){
if(autoComplete.eaemExtended){
return;
}

extendPicker(autoComplete);

autoComplete.eaemExtended = true;
});
}

function extendPicker(pathField){
var origShowPicker = pathField._showPicker;

pathField._showPicker = function(){
origShowPicker.call(this);

var columnView = $(this._picker.el).find("coral-columnview")[0];

columnView.on("coral-columnview:navigate", showFullName);

var dummyEvent = { detail : { column: $(columnView).find("coral-columnview-column")[0] } };

showFullName(dummyEvent);
}
}

function showFullName(event){
var $item, $content, $title, $thumbnail;

$(event.detail.column).find("coral-columnview-item").each(function(index, item){
$item = $(item);

$content = $item.find("coral-columnview-item-content");

$title = $content.find(".foundation-collection-item-title");

if(_.isEmpty($title) || !isEllipsisActive($title[0])){
return;
}

$item.css("height", "auto");

$content.css("height", "auto");

$title.css("height","auto").css("white-space", "normal");

$thumbnail = $item.find("coral-columnview-item-thumbnail");

$thumbnail.css("display", "flex")
.css("align-items", "center").css("height", $item.css("height"));
});
}

function isEllipsisActive(e) {
return (e.offsetWidth < e.scrollWidth);
}
}(jQuery, jQuery(document)));

AEM 6510 - Extend Asset Share Commons Date Filter to provide Relative Date Lower Bound Entry

$
0
0

Goal


Just a simple component extending Asset Share Commons Date Filterhttps://adobe-marketing-cloud.github.io/asset-share-commons/pages/search/date-range/

Provides a text box to enter query builder relativedaterange lowerBoundhttps://helpx.adobe.com/experience-manager/6-5/sites/developing/using/querybuilder-predicate-reference.html#relativedaterange

Demo | Package Install | Github


Configuration



Rendering



Solution


1) Create component dialog /apps/eaem-asset-share-date-filter/date-range/cq:dialog by extending the Asset Share Commons Date Filter dialog asset-share-commons/components/search/date-range and use render conditions granite:rendercondition to not display unnecessary tabs

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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="Date Range Filter"
sling:resourceType="cq/gui/components/authoring/dialog"
extraClientlibs="[core.wcm.components.form.options.v1.editor,asset-share-commons.author.dialog]">
<content
granite:class="cmp-options--editor-v1"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<options
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<dialog
granite:class="foundation-layout-util-vmargin"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maxmized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<tab1
jcr:primaryType="nt:unstructured"
jcr:title="Filter"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Legend to describe the role of the field."
fieldLabel="Title"
name="./jcr:title"
required="{Boolean}true"/>
<property
jcr:primaryType="nt:unstructured"
sling:orderBefore="name"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldDescription="The name of the field, which is submitted with the form data."
fieldLabel="Date Property"
metadataFieldTypes="[datetime]"
name="./property"
required="{Boolean}true">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="asset-share-commons/data-sources/filterable-properties"/>
</property>
<expanded
jcr:primaryType="nt:unstructured"
sling:orderBefore="name"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldDescription="Select if the field set should start in an expanded state (not applicable for drop down)"
name="./expanded"
text="Start expanded"
value="true"/>
<date-types
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden"/>
</items>
</column>
</items>
</tab1>
<search-behavior-tab
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden">
<granite:rendercondition
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/renderconditions/simple"
expression="false"/>
</search-behavior-tab>
</items>
</tabs>
</items>
</dialog>
</items>
</columns>
</items>
</options>
</items>
</content>
</jcr:root>


2) Create a use api class apps.experienceaem.assets.EAEMDatePredicate for reading the dialog data in HTL file

package apps.experienceaem.assets;

import com.adobe.aem.commons.assetshare.util.PredicateUtil;
import com.adobe.cq.sightly.WCMUsePojo;
import com.day.cq.search.eval.DateRangePredicateEvaluator;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EAEMDatePredicate extends WCMUsePojo {
private static final Logger log = LoggerFactory.getLogger(EAEMDatePredicate.class);

private static final String REQUEST_ATTR_FORM_ID_TRACKER = "asset-share-commons__form-id";
private static final String COMPONENT_NAME_IN_PAGE = "date_range";
private static final int INITIAL_GROUP_NUM = 99999;

private ValueMap resourceProps;
private String property;
private int group;

@Override
public void activate() {
Resource resource = getResource();

resourceProps = ResourceUtil.getValueMap(resource);
property = resourceProps.get("property", "");
group = INITIAL_GROUP_NUM;

String compName = COMPONENT_NAME_IN_PAGE + "_";
String resName = resource.getName();

if(resName.contains(compName)) {
group = Integer.parseInt(resName.substring(resName.lastIndexOf("_") + 1));
}
}

public boolean isReady() {
return StringUtils.isNotEmpty(property);
}

public String getTitle() {
return resourceProps.get("jcr:title", "");
}

public String getGroup() {
return group + "_group";
}

public String getName() {
return "relativedaterange";
}

public String getProperty() {
return property;
}

public boolean isExpanded() {
return Boolean.valueOf(resourceProps.get("expanded", "false"));
}

public String getFormId() {
SlingHttpServletRequest request = getRequest();

if (request.getAttribute(REQUEST_ATTR_FORM_ID_TRACKER) == null) {
request.setAttribute(REQUEST_ATTR_FORM_ID_TRACKER, 1);
}

return REQUEST_ATTR_FORM_ID_TRACKER + "__" + String.valueOf(request.getAttribute(REQUEST_ATTR_FORM_ID_TRACKER));
}

public String getLowerBoundName() {
return getName() + "." + DateRangePredicateEvaluator.LOWER_BOUND;
}

public String getInitialLowerBound() {
return PredicateUtil.getParamFromQueryParams(getRequest(), getGroup() + "." + getLowerBoundName());
}

public String getId() {
SlingHttpServletRequest request = getRequest();
return "cmp-date-filter" + "_" + String.valueOf(request.getResource().getPath().hashCode());
}
}

3) Create the component rendering HTL script /apps/eaem-asset-share-date-filter/date-range/date-range.html with the following code

<sly data-sly-use.predicate="apps.experienceaem.assets.EAEMDatePredicate"
data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"
data-sly-test.ready="${predicate.ready}">

<input type="hidden"
form="${predicate.formId}"
name="${predicate.group}.${predicate.name}.property"
value="${predicate.property}"
data-asset-share-predicate-id="${predicate.id}"/>

<div class="ui form">
<div class="ui fluid styled accordion field">

<div class="${predicate.expanded ? 'active' : ''} title right">
<i class="dropdown icon"></i>
${predicate.title}
</div>

<div class="${predicate.expanded ? 'active' : ''} content field"
data-asset-share-id="${predicate.id}__fields">

Units of time

<span style="margin-left: 10px ;font-size: 12px">
e.g. <b>-2m</b> = past 2 minutes | <b>-3h</b> = past 3 hours | <b>-4d</b> = past 4 days | <b>-5M</b> = past 5 months | <b>-6y</b> = past 6 years
</span>

<div style="margin-top: 10px">
<input type="text" style="width: 60%"
value="${predicate.initialLowerBound}"
form="${predicate.formId}"
maxlength="9"
for="${predicate.id}"
name="${predicate.group}.${predicate.name}.lowerBound"/>
</div>
</div>

<div class="active content field">
</div>
</div>
</div>

</sly>
<sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=!ready}"></sly>

AEM 6510 - Authoring restrict MSM Rollout to specific User Groups

$
0
0

Goal


Provide MSM (Multi Site Manager) Rollout functionality in authoring to specific User Groups. In this sample, its available to users of group administrators

Demo | Package Install | Github




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-msm-allow-rollout-to-specific-groups

2) Create node /apps/eaem-msm-allow-rollout-to-specific-groups/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.editor.sites.page], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-msm-allow-rollout-to-specific-groups/clientlib/js.txt, add

                        rollout-for-groups.js

4) Create file (nt:file) /apps/eaem-msm-allow-rollout-to-specific-groups/clientlib/rollout-for-groups.js, add the following code

(function ($, $document) {
var EDITOR_LOADED_EVENT = "cq-editor-loaded",
allowedGroup = "administrators",
extended = false;

$document.on(EDITOR_LOADED_EVENT, extendMSMOpenDialog);

function extendMSMOpenDialog(){
if(!Granite.author || !Granite.author.MsmAuthoringHelper){
console.log("Experience AEM - Granite.author.MsmAuthoringHelper not available");
return;
}

var _origFn = Granite.author.MsmAuthoringHelper.openRolloutDialog;

Granite.author.MsmAuthoringHelper.openRolloutDialog = function(dialogSource){
var userGroups = getUserGroups();

if(!userGroups.includes(allowedGroup)){
showAlert("Rollout not allowed...", "Rollout");
return;
}

_origFn.call(this, dialogSource);
};

handleEditableClick();
}

function handleEditableClick(){
$document.on("cq-overlay-click", function(){
if(extended){
return;
}

extended = true;

var _orignRolloutFn = MSM.Rollout.doRollout;

MSM.Rollout.doRollout = function(commandPath, blueprint, $targets, isBackgroundRollout) {
var userGroups = getUserGroups();

if(!userGroups.includes(allowedGroup)){
showAlert("Rollout not allowed...", "Rollout");
return;
}

_orignRolloutFn.call(this, commandPath, blueprint, $targets, isBackgroundRollout);
}
});
}

function getUserGroups(){
var userID = Granite.author.ContentFrame.getUserID(), userGroups;

$.ajax( {
url: "/bin/security/authorizables.json?filter=" + userID,
async: false
} ).done(handler);

function handler(data){
if(!data || !data.authorizables){
return;
}

_.each(data.authorizables, function(authObj){
if( (authObj.id !== userID) || _.isEmpty(authObj.memberOf)){
return;
}

userGroups = _.pluck(authObj.memberOf, "id");
});
}

return userGroups;
}

function showAlert(message, title, callback){
var fui = $(window).adaptTo("foundation-ui"),
options = [{
id: "ok",
text: "OK",
primary: true
}];

message = message || "Unknown Error";
title = title || "Error";

fui.prompt(title, message, "default", options, callback);
}
}(jQuery, jQuery(document)));

AEM 6510 - Assets Sort Folders while applying Metadata Schema

$
0
0

Goal


Sort folders while applying a metadata schema, Tools > Assets > Metadata Schemas /mnt/overlay/dam/gui/content/metadataschemaeditor/schemalist.html

Demo | Package Install | Github


Product



Extension



Solution


1) Create filter apps.experienceaem.assets.EAEMSortFolders for intercepting requests to url /mnt/overlay/dam/gui/content/processingprofilepage/selectfolderwizard/destination.html and include sort parameter sortName=name 

package apps.experienceaem.assets;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import java.io.IOException;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Datasource Sort Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=^(/mnt/overlay/dam/gui/content/processingprofilepage/selectfolderwizard/destination).*$"
}
)
public class EAEMSortFolders implements Filter {
public static String SORT_NAME = "sortName";

public static String SORT_NAME_NAME = "name";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;

String orderBy = slingRequest.getParameter(SORT_NAME);

if(StringUtils.isNotEmpty(orderBy)){
chain.doFilter(request, response);
return;
}

SlingHttpServletRequest nameSortRequest = new NameSortSlingServletRequestWrapper(slingRequest);
chain.doFilter(nameSortRequest, response);
}

@Override
public void destroy() {
}

private class NameSortSlingServletRequestWrapper extends SlingHttpServletRequestWrapper {
public NameSortSlingServletRequestWrapper(final SlingHttpServletRequest request) {
super(request);
}

@Override
public String getParameter(String paramName) {
if(!EAEMSortFolders.SORT_NAME.equals(paramName)){
return super.getParameter(paramName);
}

return EAEMSortFolders.SORT_NAME_NAME;
}
}
}

AEM 6520 - AEM Assets Search Provide Sorting on Name, Modified, Path

$
0
0

Goal


AEM otb does not provide sort in Assets Search - http://localhost:4502/aem/search.html. This extension adds sort capability on columns Name, Modified and Path (also added by extension)

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-assets-search-add-path-provide-sort

2) Create node /apps/eaem-assets-search-add-path-provide-sort/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.common.admin.searchpanel], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-assets-search-add-path-provide-sort/clientlib/js.txt, add

                        add-path-and-sort.js

4) Create file (nt:file) /apps/eaem-assets-search-add-path-provide-sort/clientlib/add-path-and-sort.js, add the following code

(function ($, $document) {
var FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
GRANITE_OMNI_SEARCH_RESULT = "#granite-omnisearch-result",
EAEM_SEARCH_PATH_COLUMN = "eaem-search-path-column",
EAEM_SEARCH_PATH_COLUMN_HEADER = "Path",
ROW_SELECTOR = "tr.foundation-collection-item",
EAEM_SORT_PARAMETER = "eaem-search-parameter",
SORT_DIRECTION_STORAGE_KEY = "apps.experienceaem.assets.searchSortDirection",
STORAGE = window.localStorage,
GRANITE_OMNI_SEARCH_CONTENT = ".granite-omnisearch-content";

$document.on(FOUNDATION_CONTENT_LOADED, GRANITE_OMNI_SEARCH_CONTENT, function(event){
_.defer(function(){
handleContentLoad(event);
});
});

$document.ready(function(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT);

STORAGE.removeItem(SORT_DIRECTION_STORAGE_KEY);

addSortParameter($form, "nodename", "asc");
});

function removeSortParameter(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT),
$sortParam = $form.find("." + EAEM_SORT_PARAMETER);

if(!_.isEmpty($sortParam)){
$sortParam.remove();
}
}

function addSortParameter($form, parameter, direction){
removeSortParameter();

$form.append(getSortHtml(parameter, direction));
}

function getSortHtml(parameter, direction){
return "<span class='" + EAEM_SORT_PARAMETER + "'>" +
"<input type='hidden' name='orderby' value='" + parameter + "'/>" +
"<input type='hidden' name='orderby.sort' value='" + direction + "'/>" +
"</span>"
}

function handleContentLoad(event){
var layout = $(GRANITE_OMNI_SEARCH_RESULT).data("foundationLayout");

if(!layout || (layout.layoutId !== "list")){
return;
}

addColumnHeaders();

fillColumnData();
}

function handleSort(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT),
$th = $(this), sortBy = "nodename",
thContent = $th.find("coral-table-headercell-content").html().trim(),
direction = "ascending";

if($th.attr("sortabledirection") == "ascending"){
direction = "descending";
}

$th.attr("sortabledirection", direction);

if(thContent == "Modified"){
sortBy = "@jcr:content/cq:lastModified";
}else if(thContent == "Path"){
sortBy = "path";
}

STORAGE.setItem(SORT_DIRECTION_STORAGE_KEY, sortBy + "=" + direction);

addSortParameter($form, sortBy, (direction == "descending" ? "desc" : "asc"));

$form.submit();
}

function fillColumnData(){
var $fui = $(window).adaptTo("foundation-ui");

$fui.wait();

$(ROW_SELECTOR).each(function(index, item){
itemHandler($(item) );
});

function itemHandler($row){
if(!_.isEmpty($row.find("[" + EAEM_SEARCH_PATH_COLUMN + "]"))){
return;
}

if(_.isEmpty($row.find("td.foundation-collection-item-title"))){
return;
}

var itemPath = $row.data("foundation-collection-item-id");

$row.find("td:last").before(getListCellHtml(itemPath));
}

$fui.clearWait();
}

function getListCellHtml(colValue){
return '<td is="coral-table-cell"' + EAEM_SEARCH_PATH_COLUMN + '>' + colValue + '</td>';
}

function addColumnHeaders(){
if(checkIFHeadersAdded()){
return;
}

var $container = $(GRANITE_OMNI_SEARCH_CONTENT),
$headRow = $container.find("thead > tr"),
sortBy, direction,
$pathCol = $(getTableHeader(EAEM_SEARCH_PATH_COLUMN_HEADER)).appendTo($headRow).click(handleSort);

sortBy = STORAGE.getItem(SORT_DIRECTION_STORAGE_KEY);

if(_.isEmpty(sortBy)){
sortBy = "nodename";
direction = "ascending";
}else{
direction = sortBy.substring(sortBy.lastIndexOf("=") + 1);
sortBy = sortBy.substring(0, sortBy.lastIndexOf("="));
}

var $nameCol = $headRow.find("th:eq(" + getIndex($headRow, "Name") + ")")
.attr("sortabledirection", "default")
.attr("sortable", "sortable").click(handleSort);

var $modifiedCol = $headRow.find("th:eq(" + getIndex($headRow, "Modified") + ")")
.attr("sortabledirection", "default")
.attr("sortable", "sortable").click(handleSort);

if(sortBy == "@jcr:content/cq:lastModified"){
$modifiedCol.attr("sortabledirection", direction)
}else if(sortBy == "path"){
$pathCol.attr("sortabledirection", direction)
}else{
$nameCol.attr("sortabledirection", direction);
}
}

function getIndex($headRow, header){
return $headRow.find("th coral-table-headercell-content:contains('" + header + "')").closest("th").index();
}

function getTableHeader(colText) {
return '<th is="coral-table-headercell" sortabledirection="default" sortable ' + EAEM_SEARCH_PATH_COLUMN + '>'
+ colText
+ '</th>';
}

function checkIFHeadersAdded(){
return !_.isEmpty($(GRANITE_OMNI_SEARCH_CONTENT).find("tr").find("[" + EAEM_SEARCH_PATH_COLUMN + "]"));
}
})(jQuery, jQuery(document));

AEM 6520 - AEM Assets Configure Default Thumbnails for Various File Types

$
0
0

Goal


In AEM Assets, configure default thumbnails for different file types and show them in List/Card/Column of Assets console, Search console

Demo | Package Install | Github


Thumbnail for MP3 - Product



Thumbnail for MP3 - Extension



Tools Extension



Configure Default Thumbnails

                                http://localhost:4502/apps/eaem-assets-default-thumbnails/select-default-thumbnails.html



Stored in CRX

                                /conf/global/settings/dam/eaem-thumbnails



Card View



List View



Column View



Search Results



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-assets-default-thumbnails

2) Add a multifield renderer /apps/eaem-assets-default-thumbnails/thumbnail-multifield/thumbnail-multifield.jsp extending otb /libs/granite/ui/components/coral/foundation/form/multifield with the following code

<%@ page import="com.adobe.granite.ui.components.Value" %>
<%@include file="/libs/granite/ui/global.jsp" %>

<%
String THUMBNAILS_PATH = "/conf/global/settings/dam/eaem-thumbnails";

slingRequest.setAttribute(Value.CONTENTPATH_ATTRIBUTE, THUMBNAILS_PATH);
%>

<sling:include resourceType="/libs/granite/ui/components/coral/foundation/form/multifield" />

3) Create cq:Page /apps/eaem-assets-default-thumbnails/select-default-thumbnails for configuring the thumbnails for file extensions

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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
jcr:mixinTypes="[sling:VanityPath]"
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM Default Thumbnails"
sling:resourceType="granite/ui/components/coral/foundation/page">
<head jcr:primaryType="nt:unstructured">
<favicon
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
<viewport
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
<clientlibs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral]"/>
</head>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/body">
<items jcr:primaryType="nt:unstructured">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form"
action="/conf/global/settings/dam/eaem-thumbnails"
foundationForm="{Boolean}true"
maximized="{Boolean}true"
method="post"
novalidate="{Boolean}true"
style="vertical">
<successresponse
jcr:primaryType="nt:unstructured"
jcr:title="Success"
sling:resourceType="granite/ui/components/coral/foundation/form/responses/openprompt"
open="/assets.html"
redirect="/apps/eaem-assets-default-thumbnails/select-default-thumbnails.html/conf/global/settings/dam/eaem-thumbnails"
text="Thumbnails configuration saved"/>
<items jcr:primaryType="nt:unstructured">
<type
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden"
name="./jcr:primaryType"
value="nt:unstructured"/>
<wizard
jcr:primaryType="nt:unstructured"
jcr:title="Configure Thumbnails"
sling:resourceType="granite/ui/components/coral/foundation/wizard">
<items jcr:primaryType="nt:unstructured">
<area
jcr:primaryType="nt:unstructured"
jcr:title="Configure Thumbnails"
sling:resourceType="granite/ui/components/coral/foundation/container"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<thumbnails
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-assets-default-thumbnails/thumbnail-multifield"
composite="{Boolean}true"
fieldLabel="Thumbnails">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./thumbnails">
<items jcr:primaryType="nt:unstructured">
<extension
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="File Extension (no period) eg. DOCX"
fieldLabel="File Extension"
name="./extension"/>
<image
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldDescription="Select Image Path"
fieldLabel="Thumbnail Path"
name="./path"
rootPath="/content/dam"/>
</items>
</field>
</thumbnails>
</items>
</column>
</items>
</columns>
</items>
<parentConfig jcr:primaryType="nt:unstructured">
<prev
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
href="/aem/start.html"
text="Cancel">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="cancel"/>
</prev>
<next
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/button"
text="Save"
type="submit"
variant="primary">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="next"/>
</next>
</parentConfig>
</area>
</items>
</wizard>
</items>
</content>
</items>
</body>
</jcr:content>
</jcr:root>

4) Add a navigation item in Tools by creating node  /apps/cq/core/content/nav/tools/eaem/asset-default-thumbnail with the following code

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured">
<tools jcr:primaryType="nt:unstructured">
<eaem
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM"
id="experience-aem-tools">
<asset-default-thumbnails
jcr:description="Experience AEM Assets Default Thumbnails"
jcr:primaryType="nt:unstructured"
jcr:title="Assets Default Thumbnails"
href="/apps/eaem-assets-default-thumbnails/select-default-thumbnails.html/conf/global/settings/dam/eaem-thumbnails"
icon="asset"
id="eaem-assets-default-thumbnails"
size="XL"/>
</eaem>
</tools>
</jcr:root>

2) Create node /apps/eaem-assets-default-thumbnails/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.common.wcm], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-assets-default-thumbnails/clientlib/js.txt, add

                        default-thumbnails.js

4) Create file (nt:file) /apps/eaem-assets-default-thumbnails/clientlib/default-thumbnails.js, add the following code

(function ($, $document) {
var THUMBNAILS_PATH = "/conf/global/settings/dam/eaem-thumbnails/thumbnails.2.json",
LAYOUT_COL_VIEW = "column",
LAYOUT_LIST_VIEW = "list",
LAYOUT_CARD_VIEW = "card",
CONTAINER = ".cq-damadmin-admin-childpages",
FOUNDATION_COLLECTION_ITEM_ID = "foundationCollectionItemId",
DIRECTORY = "directory",
CORAL_COLUMNVIEW_PREVIEW = "coral-columnview-preview",
COLUMN_VIEW = "coral-columnview",
colViewListenerAdded = false,
DEFAULT_THUMBS = {};

loadDefaultThumbnails();

$document.on("foundation-contentloaded", showThumbnails);

$document.on("foundation-selections-change", function(){
getUIWidget(CORAL_COLUMNVIEW_PREVIEW).then(showThumbnailInColumnViewPreview);
});

function showThumbnails(){
if(isColumnView()){
addColumnViewThumbnails();
}else{
addCardListViewThumbnails();
}
}

function addCardListViewThumbnails(){
var cardThumbs = DEFAULT_THUMBS.card,
listThumbs = DEFAULT_THUMBS.list;

if(_.isEmpty(cardThumbs) || _.isEmpty(listThumbs)){
return;
}

$(".foundation-collection-item").each(function(index, item){
var $item = $(item),
isFolder = ($item.data("item-type") == DIRECTORY);

if(isFolder){
return;
}

var extension = getExtension($item.data(FOUNDATION_COLLECTION_ITEM_ID));

if(_.isEmpty(cardThumbs[extension])){
return;
}

var $img = $item.find("td:first > img");

if(!isRendition($img.attr("src"))){
$img.attr("src", listThumbs[extension]);
}

$img = $item.find("coral-card-asset > img");

if(!isRendition($img.attr("src"))){
$img.attr("src", cardThumbs[extension]);
}
});
}

function addColumnViewThumbnails(){
if(colViewListenerAdded){
return;
}

var $columnView = $(COLUMN_VIEW),
columnThumbs = DEFAULT_THUMBS.column;

if(_.isEmpty($columnView) || _.isEmpty(columnThumbs)){
return;
}

colViewListenerAdded = true;

$columnView[0].on("coral-columnview:navigate", showThumbnail);

_.each($columnView.find("coral-columnview-column"), function(colItem){
showThumbnail({ detail : { column: colItem } });
});

function showThumbnail(event){
$(event.detail.column).find("coral-columnview-item").each(function(index, item){
var $item = $(item),
extension = getExtension($item.data(FOUNDATION_COLLECTION_ITEM_ID));

var $img = $item.find("coral-columnview-item-thumbnail > img");

if(!isRendition($img.attr("src"))){
$img.attr("src", columnThumbs[extension]);
}
});
}
}

function showThumbnailInColumnViewPreview($colPreview){
var columnThumbs = DEFAULT_THUMBS.column;

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

var extension = getExtension($colPreview.data("foundationLayoutColumnviewColumnid"));

var $img = $colPreview.find("coral-columnview-preview-asset > img");

if(!isRendition($img.attr("src"))){
$img.attr("src", columnThumbs[extension]);
}
}

function getExtension(path){
var extension = "";

if(_.isEmpty(path) || !path.includes(".")){
return extension;
}

extension = path.substring(path.lastIndexOf(".") + 1);

return extension.toUpperCase();
}

function loadDefaultThumbnails(){
$.ajax( { url: THUMBNAILS_PATH, asyc: false }).done(handler);

var extension = "";

function handler(data){
if(_.isEmpty(data)){
return;
}

DEFAULT_THUMBS["card"] = {};
DEFAULT_THUMBS["list"] = {};
DEFAULT_THUMBS["column"] = {};

_.each(data, function(thumb){
if(_.isEmpty(thumb.extension)){
return;
}

extension = thumb.extension.toUpperCase();

DEFAULT_THUMBS["card"][extension] = thumb.path + "/jcr:content/renditions/cq5dam.thumbnail.319.319.png";
DEFAULT_THUMBS["column"][extension] = thumb.path + "/jcr:content/renditions/cq5dam.thumbnail.319.319.png";
DEFAULT_THUMBS["list"][extension] = thumb.path + "/jcr:content/renditions/cq5dam.thumbnail.48.48.png";
})
}
}

function isColumnView(){
return ( getAssetsConsoleLayout() === LAYOUT_COL_VIEW );
}

function getAssetsConsoleLayout(){
var $childPage = $(CONTAINER),
foundationLayout = $childPage.data("foundation-layout");

if(_.isEmpty(foundationLayout)){
return "";
}

return foundationLayout.layoutId;
}

function getUIWidget(selector){
if(_.isEmpty(selector)){
return;
}

var deferred = $.Deferred();

var INTERVAL = setInterval(function(){
var $widget = $(selector);

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

clearInterval(INTERVAL);

deferred.resolve($widget);
}, 100);

return deferred.promise();
}

function isRendition(imgSrc){
return (!_.isEmpty(imgSrc) && imgSrc.includes("/renditions/"));
}

}(jQuery, jQuery(document)));

AEM 6520 - AEM Assets Search custom Metadata Columns and Sorting

$
0
0

Goal


Add custom metadata columns and support sorting (Name, Modified and on Custom Columns) in Asset Search - http://localhost:4502/aem/search.html

Similar posts for adding custom columns discussed here and here may cause wonky UI behavior (columns adjusting in UI when there are too many results etc.); the following solution works by parsing results before loading them in UI providing seamless experience...

Demo | Package Install | Github


Configure Columns



Search Results (with Sorting)



Solution


1) Login to CRXDE and create nt:folder /apps/eaem-assets-metadata-columns-sort-search

2) Configure the columns in /apps/eaem-assets-metadata-columns-sort-search/columns

3) Create a clientlib /apps/eaem-assets-metadata-columns-sort-search/clientlib with categories cq.gui.common.admin.searchpanel, dependencies lodash

4) Add /apps/eaem-assets-metadata-columns-sort-search/clientlib/js.txt with the following content

                     metadata-columns.js

5) Add file (nt:file) /apps/eaem-assets-metadata-columns-sort-search/clientlib/metadata-columns.js, add the following code

(function ($, $document) {
var FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
GRANITE_OMNI_SEARCH_RESULT = "#granite-omnisearch-result",
EAEM_METADATA_REL_PATH = "data-eaem-metadata-rel-path",
ROW_SELECTOR = "tr.foundation-collection-item",
EAEM_SEARCH_PATH_COLUMN_HEADER = "Path",
GRANITE_OMNI_SEARCH_CONTENT = ".granite-omnisearch-content",
EAEM_META_COLUMNS_URL = "/apps/eaem-assets-metadata-columns-sort-search/columns.1.json",
STORAGE = window.localStorage,
EAEM_SORT_PARAMETER = "eaem-search-parameter",
SORT_DIRECTION_STORAGE_KEY = "apps.eaem.assets.searchSortDirection",
PARSER_KEY = "foundation.adapters.internal.adapters.foundation-util-htmlparser",
metaParams = {}, sortHandlersAdded = false;

loadCustomColumnHeaders();

extendHtmlParser();

$document.ready(function(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT);

STORAGE.removeItem(SORT_DIRECTION_STORAGE_KEY);

addSortParameter($form, "nodename", "asc");
});

$document.on(FOUNDATION_CONTENT_LOADED, GRANITE_OMNI_SEARCH_CONTENT, function(event){
_.defer(function(){
handleContentLoad(event);
});
});

function handleContentLoad(event){
var layout = $(GRANITE_OMNI_SEARCH_RESULT).data("foundationLayout");

if(sortHandlersAdded || !layout || (layout.layoutId !== "list")){
return;
}

sortHandlersAdded = true;

var $container = $(GRANITE_OMNI_SEARCH_CONTENT),
$headRow = $container.find("thead > tr"),
sortBy = STORAGE.getItem(SORT_DIRECTION_STORAGE_KEY), direction;

if(_.isEmpty(sortBy)){
sortBy = "nodename";
direction = "ascending";
}else{
direction = sortBy.substring(sortBy.lastIndexOf("=") + 1);
sortBy = sortBy.substring(0, sortBy.lastIndexOf("="));
}

addSortHandler($headRow,"Name", "nodename", sortBy, direction);

addSortHandler($headRow,"Modified", "@jcr:content/jcr:lastModified", sortBy, direction);

addSortHandler($headRow,"EAEM Desc", "@jcr:content/metadata/eaemDesc", sortBy, direction);

addSortHandler($headRow,"EAEM Keywords", "@jcr:content/metadata/eaemKeywords", sortBy, direction);

addSortHandler($headRow,"EAEM Title", "@jcr:content/metadata/eaemTitle", sortBy, direction);
}

function removeSortParameter(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT),
$sortParam = $form.find("." + EAEM_SORT_PARAMETER);

if(!_.isEmpty($sortParam)){
$sortParam.remove();
}
}

function addSortParameter($form, parameter, direction){
removeSortParameter();

$form.append(getSortHtml(parameter, direction));
}

function getSortHtml(parameter, direction){
return "<span class='" + EAEM_SORT_PARAMETER + "'>" +
"<input type='hidden' name='orderby' value='" + parameter + "'/>" +
"<input type='hidden' name='orderby.sort' value='" + direction + "'/>" +
"</span>"
}

function addSortHandler($headRow, header, sortBy, metaPath, direction){
var $col = $headRow.find("th:eq(" + getIndex($headRow, header) + ")")
.attr("sortabledirection", "default")
.attr("sortable", "sortable").click(handleSort);

if(sortBy == metaPath){
$col.attr("sortabledirection", direction)
}

return $col;
}

function handleSort(){
var $form = $(GRANITE_OMNI_SEARCH_CONTENT),
$th = $(this), sortBy = "nodename",
thContent = $th.find("coral-table-headercell-content").html().trim(),
direction = "ascending";

if($th.attr("sortabledirection") == "ascending"){
direction = "descending";
}

$th.attr("sortabledirection", direction);

if(thContent == "Modified"){
sortBy = "@jcr:content/jcr:lastModified";
}else if(thContent == "EAEM Title"){
sortBy = "@jcr:content/metadata/eaemTitle";
}else if(thContent == "EAEM Desc"){
sortBy = "@jcr:content/metadata/eaemDesc";
}else if(thContent == "EAEM Keywords"){
sortBy = "@jcr:content/metadata/eaemKeywords";
}

STORAGE.setItem(SORT_DIRECTION_STORAGE_KEY, sortBy + "=" + direction);

addSortParameter($form, sortBy, (direction == "descending" ? "desc" : "asc"));

$form.submit();
}

function extendHtmlParser(){
var htmlParser = $(window).data(PARSER_KEY),
otbParse = htmlParser.instance.parse;

htmlParser.instance.parse = function(html, avoidMovingExisting){
var $parsedResponse = $(html);

if(!_.isEmpty($parsedResponse.find(GRANITE_OMNI_SEARCH_RESULT))){
sortHandlersAdded = false;

addCustomHeaders($parsedResponse);

fillColumnData(fetchCustomColumnValues(), $parsedResponse);
}else if( GRANITE_OMNI_SEARCH_RESULT == ("#" + $parsedResponse.attr("id"))){
fillColumnData(fetchCustomColumnValues(), $parsedResponse);

html = $parsedResponse[0].outerHTML;
}

return otbParse.call(htmlParser.instance, html, avoidMovingExisting);
}
}

function addCustomHeaders($parsedResponse){
var $container = $parsedResponse.find(GRANITE_OMNI_SEARCH_RESULT),
$headRow = $container.find("thead > tr");

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

_.each(metaParams, function(header, metaPath){
$headRow.append(getTableHeader(header, metaPath));
});

$(getTableHeader(EAEM_SEARCH_PATH_COLUMN_HEADER, "jcr:path")).appendTo($headRow);
}

function fillColumnData(results, $parsedResponse){
$parsedResponse.find(ROW_SELECTOR).each(function(index, item){
itemHandler($(item) );
});

function itemHandler($row){
if(!_.isEmpty($row.find("[" + EAEM_METADATA_REL_PATH + "]"))){
return;
}

if(_.isEmpty($row.find("td.foundation-collection-item-title"))){
return;
}

var itemPath = $row.data("foundation-collection-item-id"),
metadata, metaProp, $td = $row.find("td:last");

_.each(metaParams, function(header, metaPath){
metadata = (results[itemPath] || {});

metaProp = metaPath.substring(metaPath.lastIndexOf("/") + 1);

$td = $(getListCellHtml(metaPath, metadata[metaProp])).insertAfter($td);
});

$td = $(getListCellHtml("jcr:path", itemPath)).insertAfter($td);

$(getEmptyListCell()).insertAfter($td);
}
}

function fetchCustomColumnValues() {
var $form = $("form.foundation-form"), results = {},
query = "/bin/querybuilder.json?" + $form.serialize();

query = query + "&999_property=jcr:primaryType&999_property.value=dam:Asset&p.hits=selective&p.limit=-1&p.properties=jcr:path";

query = query + "+" + Object.keys(metaParams).join("+");

$.ajax({ url: query, async: false }).done(function(data){
if(!data || (data.results <= 0) ){
return;
}

_.each(data.hits, function(hit){
results[hit["jcr:path"]] = hit["jcr:content"]["metadata"];
});
});

return results;
}

function getEmptyListCell(){
return '<td is="coral-table-cell" style="display:none"></td>';
}

function getIndex($headRow, header){
return $headRow.find("th coral-table-headercell-content:contains('" + header + "')").closest("th").index();
}

function getListCellHtml(metaPath, metaValue){
metaValue = (metaValue || "");

return '<td is="coral-table-cell"' + EAEM_METADATA_REL_PATH + '="' + metaPath + '">' + metaValue + '</td>';
}

function getTableHeader(colText, metadataPath) {
return '<th is="coral-table-headercell"' + EAEM_METADATA_REL_PATH + '="' + metadataPath + '">' + colText + '</th>';
}

function loadCustomColumnHeaders(){
$.ajax( { url: EAEM_META_COLUMNS_URL, async: false} ).done(function(data){
_.each(data, function(colData){
if(_.isEmpty(colData.header) || _.isEmpty(colData.metadataPath)){
return;
}

metaParams[colData.metadataPath] = colData.header;
});
});
}
})(jQuery, jQuery(document));


AEM 6520 - Change Background color and Placeholder text of New Layout Container

$
0
0

Goal


Provide custom background color and placeholder text for Layout Container (wcm/foundation/components/responsivegrid) in Authoring - /editor.html

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-layout-container-placeholder

2) Create node /apps/eaem-layout-container-placeholder/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.dialog.all], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-layout-container-placeholder/clientlib/js.txt, add

                        placeholder.js

4) Create file (nt:file) /apps/eaem-layout-container-placeholder/clientlib/placeholder.js, add the following code

(function ($, $document) {
var BG_COLOR = "#C7B097",
PLACEHOLDER_TEXT = "Experience AEM - Drag components here",
LAYOUT_CONTAINER_NEW = "wcm/foundation/components/responsivegrid/new";

$document.on("cq-editable-added", function(event){
modifyLayoutContainer([event.editable]);
});

$document.on('cq-layer-activated', function(){
modifyLayoutContainer(Granite.author.editables);
});

function modifyLayoutContainer(editables){
_.each(editables, function(editable){
if(!editable || !(editable.type == LAYOUT_CONTAINER_NEW)){
return;
}

if(!editable.overlay || !editable.overlay.dom){
editable.dom.css("background-color", BG_COLOR).attr("data-text", PLACEHOLDER_TEXT);
return;
}

//for new layout containers, Granite.author.Inspectable.prototype.hasPlaceholder()
//always returns "Drag components here"
editable.overlay.dom.css("background-color", BG_COLOR).attr("data-text", PLACEHOLDER_TEXT);
});
}
}(jQuery, jQuery(document)));

AEM 6520 - AEM Assets Timeline show Version name

$
0
0

Goal


In AEM Assets Timeline rail show the Version File Name. Showing names could be useful when the filename change frequently (say work in progress assets)

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-version-timeline-show-name

2) Create node /apps/eaem-version-timeline-show-name/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.coral.common.admin.timeline], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-version-timeline-show-name/clientlib/js.txt, add

                        show-filename.js

4) Create file (nt:file) /apps/eaem-version-timeline-show-name/clientlib/show-filename.js, add the following code

(function($, $document) {
var TIME_LINE_EVENT_CSS = ".cq-common-admin-timeline-event",
EAEM_VERSION_NAME = "eaem-version-name";

$document.on("foundation-contentloaded.foundation", ".cq-common-admin-timeline-events", modifyVersionDisplay);

function modifyVersionDisplay(){
var $timelineEvents = $(TIME_LINE_EVENT_CSS), $section,
$main, $comment, versionNum;

var versionNames = loadVersionNames();

_.each($timelineEvents, function(section){
$section = $(section);

versionNum = $section.data("preview");

if(!_.isEmpty(versionNum)){
versionNum = versionNum.substring(0,versionNum.indexOf("/jcr:frozenNode"));
versionNum = versionNum.substring(versionNum.lastIndexOf("/") + 1);
}

$main = $section.find(".main");

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

if(!_.isEmpty(versionNames[versionNum]) && _.isEmpty($section.find("." + EAEM_VERSION_NAME))){
$( "<div class='" + EAEM_VERSION_NAME + "'>" + versionNames[versionNum] + "</div>" ).insertBefore( $main );
}
});
}

function loadVersionNames(){
var $events = $(TIME_LINE_EVENT_CSS), versionNames = {}, path;

_.each($events, function(event){
if(!path && !_.isEmpty($(event).data("preview"))){
path = $(event).data("preview");
}
});

if(_.isEmpty(path)){
return versionNames;
}

path = path.substring(0,path.indexOf("/jcr:frozenNode"));

path = path.substring(0,path.lastIndexOf("/")) + ".3.json";

$.ajax( {url : path, async: false}).done(function(data){
if(_.isEmpty(data)){
return;
}

_.each(data, function(value, key){
if(key.startsWith("jcr:") || key.startsWith("crx:")){
return;
}

var cqName = nestedPluck(value, "jcr:frozenNode/jcr:content/cq:name");

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

versionNames[key] = cqName;
});
});

return versionNames;
}

function nestedPluck(object, key) {
if (!_.isObject(object) || _.isEmpty(object) || _.isEmpty(key)) {
return [];
}

if (key.indexOf("/") === -1) {
return object[key];
}

var nestedKeys = _.reject(key.split("/"), function(token) {
return token.trim() === "";
}), nestedObjectOrValue = object;

_.each(nestedKeys, function(nKey) {
if(_.isUndefined(nestedObjectOrValue)){
return;
}

if(_.isUndefined(nestedObjectOrValue[nKey])){
nestedObjectOrValue = undefined;
return;
}

nestedObjectOrValue = nestedObjectOrValue[nKey];
});

return nestedObjectOrValue;
}
})(jQuery, jQuery(document));

AEM 6520 - AEM Assets add Custom Metadata Columns in Assets Console List View

$
0
0

Goal


Add Custom metadata columns in List view of Assets Consolehttp://localhost:4502/assets.html/

This post adds columns using server side rendering, for adding columns using client side logic check this post

For Custom metadata columns in Search Consolecheck this post

Demo | Package Install | Github


Configure Columns



Columns in List View



Solution


1) Add the custom metadata columns configuration in /apps/dam/gui/content/commons/availablecolumns (Sling Resource Merger combines the otb columns in /libs/dam/gui/content/commons/availablecolumns with ones configured in /apps)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured">
<eaemTitle
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Title"
columnGroup="Experience AEM"
configurable="{Boolean}true"
default="{Boolean}true"/>
<eaemDesc
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Description"
columnGroup="Experience AEM"
configurable="{Boolean}true"
default="{Boolean}true"/>
<eaemKeywords
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Keywords"
columnGroup="Experience AEM"
configurable="{Boolean}true"
default="{Boolean}true"/>
</jcr:root>


2) Add the following code for rendering metadata values in /apps/dam/gui/coral/components/admin/contentrenderer/row/common/reorder.jsp; include the otb reorder.jsp using cq:include

<%@include file="/libs/granite/ui/global.jsp"%>
<%@ page import="org.apache.sling.api.resource.ValueMap" %>
<%@ page import="org.apache.sling.api.resource.Resource" %>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0"%>

<%
final String ASSET_RES_TYPE = "dam/gui/coral/components/admin/contentrenderer/row/asset";

Resource assetResource = resource;
String eaemTitle = "", eaemDesc = "", eaemKeywords = "";

if(assetResource.getResourceType().equals(ASSET_RES_TYPE)){
ValueMap vm = assetResource.getChild("jcr:content/metadata").getValueMap();

eaemTitle = (String)vm.get("eaemTitle", "");
eaemDesc = (String)vm.get("eaemDesc", "");
eaemKeywords = (String)vm.get("eaemKeywords", "");
}

%>

<td is="coral-table-cell" value="<%= eaemTitle %>">
<%= eaemTitle %>
</td>

<td is="coral-table-cell" value="<%= eaemDesc %>">
<%= eaemDesc %>
</td>

<td is="coral-table-cell" value="<%= eaemKeywords %>">
<%= eaemKeywords %>
</td>

<cq:include script = "/libs/dam/gui/coral/components/admin/contentrenderer/row/common/reorder.jsp"/>

AEM 6520 - Sites Impersonation Component

$
0
0

Goal


Create a component for impersonating users on Site's pages in author and publish

Demo | Package Install | Github


Configure Impersonating Users

                              here user nalabotu can impersonate as user cavery




Configure User Group in Dialog

                              here user nalabotu is part of administrators group and can see the textbox to enter userid for impersonation



Impersonation



Revert to Self



Solution


1) Configure the project sling models package in bundle/pom.xml

<Sling-Model-Packages>
apps.experienceaem.sites
</Sling-Model-Packages>

2) Create a sling model to support Impersonation component. This checks for the existence of sling.sudo cookie and if logged user can be shown the impersonation feature (user is part of configured group)

package apps.experienceaem.sites;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;

import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Required;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import java.util.*;

@Model(adaptables = SlingHttpServletRequest.class)
public class EAEMImpersonationModel {
private static final Logger log = LoggerFactory.getLogger(EAEMImpersonationModel.class);

private static final String PROP_IMPERSONATION_GROUP = "impersonatorsGroup";

@Self
@Required
private SlingHttpServletRequest request;

private boolean showImpersonation;

@PostConstruct
private void init() {
showImpersonation = false;

try {
if(request.getCookie("sling.sudo") != null){
showImpersonation = true;
return;
}

ResourceResolver resolver = request.getResourceResolver();

ValueMap resourceProps = ResourceUtil.getValueMap(request.getResource());
String impersonationGroup = resourceProps.get(PROP_IMPERSONATION_GROUP, "");

if(StringUtils.isEmpty(impersonationGroup)){
return;
}

Authorizable auth = resolver.adaptTo(Authorizable.class);
Iterator<Group> groups = auth.memberOf();

while(groups.hasNext()){
if(groups.next().getID().equalsIgnoreCase(impersonationGroup)){
showImpersonation = true;
return;
}
}
} catch (Exception e) {
log.error("Error getting impersonation model", e);
}
}

public boolean getShowImpersonation() {
return showImpersonation;
}
}

3) Create component /apps/eaem-impersonation-component/user-impersonation and add the following code in user-impersonation.html

<div class="ui form"
data-sly-use.impModel="apps.experienceaem.sites.EAEMImpersonationModel"
data-sly-test="${impModel.showImpersonation}">
<div id="eaem-impersonate">
<div style="margin: 0 0 15px 0">Impersonate as (enter user id)</div>
<input type="text" style="width: 100%" onchange="EAEM_IMPERSONATION.impersonateAsUser(this.value)"/>
</div>

<div id="eaem-impersonate-revert" style="display:none">
<div>
Impersonating as "<span id="eaem-impersonate-user"></span>"
</div>
<div style="margin: 10px 0 0 0; cursor: pointer;"
onclick="EAEM_IMPERSONATION.revertToSelf()">
Revert to Self
</div>
</div>
</div>

<div data-sly-test="${!impModel.showImpersonation && wcmmode.edit}">
Impersonators group not configured or logged in user not a member of the group
</div>

<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html"
data-sly-call="${clientlib.all @ categories='eaem.user.impersonation'}"/>

4) Create client library /apps/eaem-impersonation-component/user-impersonation/clientlib with categories eaem.user.impersonation, add the following code in user-impersonation.js

(function () {
window.EAEM_IMPERSONATION = {
impersonateAsUser: function (userId) {
document.cookie = "sling.sudo=" + userId + "; path=/";
location.reload();
},

revertToSelf: function () {
document.cookie = "sling.sudo=; path=/;";
location.reload();
},

checkImpersonated: function () {
var cookies = document.cookie;

if (cookies && (cookies.indexOf("sling.sudo") != -1)) {
var user = cookies.match('(^|;) ?' + 'sling.sudo' + '=([^;]*)(;|$)');

$("#eaem-impersonate").hide();

$("#eaem-impersonate-user").html(user ? user[2] : "");
$("#eaem-impersonate-revert").show();
}
}
};

EAEM_IMPERSONATION.checkImpersonated();
}());

AEM 6430 - AEM Asset Share Commons Presigned S3 Download URLs

$
0
0

Goal


Add a Search Results component /apps/eaem-asc-s3-presigned-urls/components/results extending Asset Share Commons Search Results component /apps/asset-share-commons/components/search/results to download the assets from Amazon S3 (AEM is configured with S3 Data Store)

Default download uses the Asset Download Servlet/content/dam.assetdownload.zip/assets.zip, however if the need is to offload asset downloading from AEM to a specialized service like S3, the following solution can be tried out... Downloading large files (> 1GB) directly from S3 storage can help improve the performance of AEM publish instances...

In the following solution, adding selector extension eaems3download.html to any asset path e.g. /content/dam/experience-aem/123.jpg.eaems3download.html creates a redirect (302) response with Presigned S3 url

If AEM is on Oak 1.10 or later, Direct Binary Access can also improve AEM performance by downloading binaries directly from storage provider (S3, Azure etc)

Package Install | Github


OSGI Configuration

                  Provide S3 Datastore bucket name - http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.EAEMS3Service



Component Configuration




Component Rendering




Solution


1) Assuming AEM is running on a AWS EC2 instance configured with a IAM role for accessing S3, you can use the following statement to create S3 client

                   AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();

if using accessKey and secretKey the client can be created using...

                   AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
                             .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("accessKey", "secretKey")))
                             .build();

2)  Add a service for reading the bucket configuration and creating Presigned urls apps.experienceaem.assets.EAEMS3Service. AEM creates the SHA256 of asset binary, uses it as S3 object id and stores the id in segment store (not available via asset metadata) before uploading the binary to S3. getS3AssetIdFromReference() function gets the s3 object id from binary reference...

package apps.experienceaem.assets;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.JackrabbitValue;
import org.apache.jackrabbit.api.ReferenceBinary;
import org.apache.sling.api.resource.Resource;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.Value;
import java.net.URL;
import java.util.Date;

@Component(
immediate=true ,
service={ EAEMS3Service.class }
)
@Designate(ocd = EAEMS3Service.Configuration.class)
public class EAEMS3Service {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private static AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();

private long singleFileS3Expiration = (1000 * 60 * 60);
private String s3BucketName = "";

@Activate
protected void activate(EAEMS3Service.Configuration configuration) {
singleFileS3Expiration = configuration.singleFileS3Expiration();
s3BucketName = configuration.s3BucketName();
}

public String getS3PresignedUrl(Resource resource){
String presignedUrl = "";

if( (resource == null) || !DamUtil.isAsset(resource)){
logger.warn("Resource null or not a dam:Asset");
return presignedUrl;
}

if(StringUtils.isEmpty(s3BucketName)){
logger.warn("S3 Bucket Name not configured");
return presignedUrl;
}

Asset s3Asset = DamUtil.resolveToAsset(resource);

if(s3Asset == null){
return presignedUrl;
}

try{
String objectKey = getS3AssetIdFromReference(resource);

logger.debug("Path = " + resource.getPath() + ", S3 object key = " + objectKey);

if(StringUtils.isEmpty(objectKey)){
return presignedUrl;
}

ResponseHeaderOverrides nameHeader = new ResponseHeaderOverrides();
nameHeader.setContentType(s3Asset.getMimeType());
nameHeader.setContentDisposition("attachment; filename=" + resource.getName());

GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(s3BucketName, objectKey)
.withMethod(HttpMethod.GET)
.withResponseHeaders(nameHeader)
.withExpiration(getSingleFileS3ExpirationDate());

URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

presignedUrl = url.toString();

logger.debug("Path = " + resource.getPath() + ", S3 presigned url = " + presignedUrl);
}catch(Exception e){
logger.error("Error generating s3 presigned url for " + resource.getPath());
}

return presignedUrl;
}

public Date getSingleFileS3ExpirationDate(){
Date expiration = new Date();

long expTimeMillis = expiration.getTime();
expTimeMillis = expTimeMillis + singleFileS3Expiration;

expiration.setTime(expTimeMillis);

return expiration;
}

public static String getS3AssetIdFromReference(final Resource assetResource) throws Exception {
String s3AssetId = StringUtils.EMPTY;

if( (assetResource == null) || !DamUtil.isAsset(assetResource)){
return s3AssetId;
}

Resource original = assetResource.getChild(JcrConstants.JCR_CONTENT + "/renditions/original/jcr:content");

if(original == null) {
return s3AssetId;
}

Node orgNode = original.adaptTo(Node.class);

if(!orgNode.hasProperty("jcr:data")){
return s3AssetId;
}

Property prop = orgNode.getProperty("jcr:data");

ReferenceBinary value = (ReferenceBinary)prop.getBinary();

s3AssetId = value.getReference();

if(!s3AssetId.contains(":")){
return s3AssetId;
}

s3AssetId = s3AssetId.substring(0, s3AssetId.lastIndexOf(":"));

s3AssetId = s3AssetId.substring(0, 4) + "-" + s3AssetId.substring(4);

return s3AssetId;
}

public static String getS3AssetId(final Resource assetResource) {
String s3AssetId = StringUtils.EMPTY;

if( (assetResource == null) || !DamUtil.isAsset(assetResource)){
return s3AssetId;
}

Resource original = assetResource.getChild(JcrConstants.JCR_CONTENT + "/renditions/original");

if(original == null) {
return s3AssetId;
}

//performance hit when the file size cross several MBs, GBs
Value value = (Value)original.getValueMap().get(JcrConstants.JCR_CONTENT + "/" + JcrConstants.JCR_DATA, Value.class);

if (value != null && (value instanceof JackrabbitValue)) {
s3AssetId = gets3ObjectIdFromJackrabbitValue((JackrabbitValue) value);
}

return s3AssetId;
}

private static String gets3ObjectIdFromJackrabbitValue(JackrabbitValue jrValue) {
if (jrValue == null) {
return StringUtils.EMPTY;
}

String contentIdentity = jrValue.getContentIdentity();

if (StringUtils.isBlank(contentIdentity)) {
return StringUtils.EMPTY;
}

int end = contentIdentity.lastIndexOf('#');

contentIdentity = contentIdentity.substring(0, end != -1 ? end : contentIdentity.length());

return contentIdentity.substring(0, 4) + "-" + contentIdentity.substring(4);
}

@ObjectClassDefinition(
name = "Experience AEM S3 for Download",
description = "Experience AEM S3 Presigned URLs for Download"
)
public @interface Configuration {

@AttributeDefinition(
name = "Single file download S3 URL expiration",
description = "Single file download Presigned S3 URL expiration",
type = AttributeType.LONG
)
long singleFileS3Expiration() default (1000 * 60 * 60);

@AttributeDefinition(
name = "S3 Bucket Name e.g. eaem-s3-bucket",
description = "S3 Bucket Name e.g. eaem-s3-bucket",
type = AttributeType.STRING
)
String s3BucketName();
}
}


3) Add a servlet apps.experienceaem.assets.EAEMS3DownloadServlet for creating redirect response (302) with presigned urls...

package apps.experienceaem.assets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(
service = Servlet.class,
property = {
"sling.servlet.methods=GET",
"sling.servlet.resourceTypes=dam:Asset",
"sling.servlet.selectors=eaems3download",
"sling.servlet.extensions=html"
}
)
public class EAEMS3DownloadServlet extends SlingAllMethodsServlet {

@Reference
private EAEMS3Service eaems3Service;

public final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect(eaems3Service.getS3PresignedUrl(request.getResource()));
}
}


4) Create component /apps/eaem-asc-s3-presigned-urls/components/results with sling:resourceSuperType /apps/asset-share-commons/components/search/results



5) Create the dialog /apps/eaem-asc-s3-presigned-urls/components/results/cq:dialog with following code, for adding eaemUseS3 field

<?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="Date Range Filter"
sling:resourceType="cq/gui/components/authoring/dialog"
extraClientlibs="[core.wcm.components.form.options.v1.editor,asset-share-commons.author.dialog]">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<tab-2 jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<column jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<card
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
emptyText="Choose an render for Card results"
extensionTypes="[/apps/eaem-asc-s3-presigned-urls/components/results/result/card]"
fieldDescription="Resource type used to render card results"
fieldLabel="Card Result Renderer"
name="./cardResultResourceType">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="asset-share-commons/data-sources/result-resource-types"/>
</card>
<use-s3
jcr:primaryType="nt:unstructured"
sling:orderBefore="name"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldDescription="Use AWS S3 Presigned URLs for Download"
name="./eaemUseS3"
text="S3 Presigned urls for download"
value="{Boolean}true"/>
</items>
</column>
</items>
</tab-2>
</items>
</tabs>
</items>
</content>
</jcr:root>


6) In /apps/eaem-asc-s3-presigned-urls/components/results/result/card/templates/card.html use the following code to add S3DOWNLOAD in card view

<li data-sly-test="${config.downloadEnabled}">
<div data-sly-unwrap data-sly-test="${!properties.eaemUseS3}">
<button class="ui link button"
data-asset-share-id="download-asset"
data-asset-share-asset="${asset.path}"
data-asset-share-license="${config.licenseEnabled ? asset.properties['license'] : ''}">${'Download' @ i18n}</button>
</div>
<div data-sly-unwrap data-sly-test="${properties.eaemUseS3}">
<a href="${asset.path}.eaems3download.html" target="_blank">S3DOWNLOAD</a>
</div>
</li>



AEM 6520 - AEM Assets Share Commons S3 Presigned URLs for downloading large carts 10-20 GB

$
0
0

Goal


Asset Share Commons provides Cart process for downloading assets. However, if the cart size is too big say 10-20GB, AEM might take a performance hit making it unusable for other activities. For creating such cart zips the following post throttles requests using Sling Ordered queue, Uploads the created carts to S3 (if AEM is configured with S3 data store, the same bucket is used for storing carts), creates Presigned urls and Email's users the download link

Carts created in S3 bucket are not deleted, assuming the Datastore Garbage Collection task takes care of cleaning them up from data store during routine maintenance...

For creating S3 Presigned urls for individual assets check this post

Package Install | Github


Bundle Whitelist

                    For demo purposes i used getAdministrativeResourceResolver(null) and not service resource resolver, so whitelist the bundle...

                    http://localhost:4502/system/console/configMgr/org.apache.sling.jcr.base.internal.LoginAdminWhitelist



Configure Limit

                    The direct download limit in the following screenshot was set to 50MB. Any carts less then 50MB are directly downloaded from AEM

                    Carts more than 50MB are uploaded to S3 and Presigned url is emailed to user

                    http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.EAEMS3Service




Direct Download



Email Download Link



Email




Solution


1) Add an OSGI service apps.experienceaem.assets.EAEMS3Service for creating cart zips, uploading to S3, generate Presigned URLs with the following code...

package apps.experienceaem.assets;

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferManagerBuilder;
import com.amazonaws.services.s3.transfer.Upload;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.request.RequestParameter;
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.base.util.AccessControlUtil;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Component(
immediate=true ,
service={ EAEMS3Service.class }
)
@Designate(ocd = EAEMS3Service.Configuration.class)
public class EAEMS3Service {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public static String ZIP_MIME_TYPE = "application/zip";

private static AmazonS3 s3Client = null;
private static TransferManager s3TransferManager = null;

private long cartFileS3Expiration = (1000 * 60 * 60);
private String s3BucketName = "";

private long directDownloadLimit = 52428800L; // 50 MB

@Activate
protected void activate(Configuration configuration) {
cartFileS3Expiration = configuration.cartFileS3Expiration();
s3BucketName = configuration.s3BucketName();
directDownloadLimit = configuration.directDownloadLimit();

logger.info("Creating s3Client and s3TransferManager...");

s3Client = AmazonS3ClientBuilder.defaultClient();
s3TransferManager = TransferManagerBuilder.standard().withS3Client(s3Client).build();
}

public long getDirectDownloadLimit(){
return directDownloadLimit;
}

public String getS3PresignedUrl(String objectKey, String cartName, String mimeType){
String presignedUrl = "";

try{
if(StringUtils.isEmpty(objectKey)){
return presignedUrl;
}

ResponseHeaderOverrides nameHeader = new ResponseHeaderOverrides();
nameHeader.setContentType(mimeType);
nameHeader.setContentDisposition("attachment; filename=" + cartName);

GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(s3BucketName, objectKey)
.withMethod(HttpMethod.GET)
.withResponseHeaders(nameHeader)
.withExpiration(getCartFileExpirationDate());

URL url = s3Client.generatePresignedUrl(generatePresignedUrlRequest);

presignedUrl = url.toString();

logger.debug("Cart = " + cartName + ", S3 presigned url = " + presignedUrl);
}catch(Exception e){
logger.error("Error generating s3 presigned url for " + cartName);
}

return presignedUrl;
}

public String uploadToS3(String cartName, String cartTempFilePath) throws Exception{
File cartTempFile = new File(cartTempFilePath);
PutObjectRequest putRequest = new PutObjectRequest(s3BucketName, cartName, cartTempFile);

ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(ZIP_MIME_TYPE);

putRequest.setMetadata(metadata);

Upload upload = s3TransferManager.upload(putRequest);

upload.waitForCompletion();

if(!cartTempFile.delete()){
logger.warn("Error deleting temp cart from local file system after uploading to S3 - " + cartTempFilePath);
}

return cartName;
}

public String createTempZip(List<Asset> assets, String cartName) throws Exception{
File cartFile = File.createTempFile(cartName, ".tmp");
FileOutputStream cartFileStream = new FileOutputStream(cartFile);

ZipOutputStream zipStream = new ZipOutputStream( cartFileStream );

zipStream.setMethod(ZipOutputStream.DEFLATED);
zipStream.setLevel(Deflater.NO_COMPRESSION);

assets.forEach(asset -> {
BufferedInputStream inStream = new BufferedInputStream(asset.getOriginal().getStream());

try{
zipStream.putNextEntry(new ZipEntry(asset.getName()));

IOUtils.copyLarge(inStream, zipStream);

zipStream.closeEntry();
}catch(Exception e){
logger.error("Error adding zip entry - " + asset.getPath(), e);
}finally{
IOUtils.closeQuietly(inStream);
}
});

IOUtils.closeQuietly(zipStream);

return cartFile.getAbsolutePath();
}

public String getDirectDownloadUrl(List<Asset> assets){
StringBuilder directUrl = new StringBuilder();

directUrl.append("/content/dam/.assetdownload.zip/assets.zip?flatStructure=true&licenseCheck=false&");

for(Asset asset : assets){
directUrl.append("path=").append(asset.getPath()).append("&");
}

return directUrl.toString();
}

public List<Asset> getAssets(ResourceResolver resolver, String paths){
List<Asset> assets = new ArrayList<Asset>();
Resource assetResource = null;

for(String path : paths.split(",")){
assetResource = resolver.getResource(path);

if(assetResource == null){
continue;
}

assets.add(assetResource.adaptTo(Asset.class));
}

return assets;
}

public List<Asset> getAssets(ResourceResolver resolver, RequestParameter[] requestParameters){
List<Asset> assets = new ArrayList<Asset>();

if(ArrayUtils.isEmpty(requestParameters)){
return assets;
}

for (RequestParameter requestParameter : requestParameters) {
Resource resource = resolver.getResource(requestParameter.getString());

if(resource == null){
continue;
}

assets.add(resource.adaptTo(Asset.class));
}

return assets;
}

public long getSizeOfContents(List<Asset> assets) throws Exception{
long size = 0L;
Node node, metadataNode = null;

for(Asset asset : assets){
node = asset.adaptTo(Node.class);
metadataNode = node.getNode("jcr:content/metadata");

long bytes = Long.valueOf(DamUtil.getValue(metadataNode, "dam:size", "0"));

if (bytes == 0 && (asset.getOriginal() != null)) {
bytes = asset.getOriginal().getSize();
}

size = size + bytes;
}

return size;
}

public String getCartZipFileName(String username){
if(StringUtils.isEmpty(username)){
username = "anonymous";
}

String cartName = "cart-" + username;

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

cartName = cartName + "-" + format.format(new Date()) + ".zip";

return cartName;
}

public Date getCartFileExpirationDate(){
Date expiration = new Date();

long expTimeMillis = expiration.getTime();
expTimeMillis = expTimeMillis + cartFileS3Expiration;

expiration.setTime(expTimeMillis);

return expiration;
}

public String getUserEmail(ResourceResolver resolver, String userId) throws Exception{
UserManager um = AccessControlUtil.getUserManager(resolver.adaptTo(Session.class));

Authorizable user = um.getAuthorizable(userId);
ValueMap profile = resolver.getResource(user.getPath() + "/profile").adaptTo(ValueMap.class);

return profile.get("email", "");
}

@ObjectClassDefinition(
name = "Experience AEM S3 for Download",
description = "Experience AEM S3 Presigned URLs for Downloading Asset Share Commons Carts"
)
public @interface Configuration {

@AttributeDefinition(
name = "Cart download S3 URL expiration",
description = "Cart download Presigned S3 URL expiration",
type = AttributeType.LONG
)
long cartFileS3Expiration() default (3 * 24 * 60 * 60 * 1000 );

@AttributeDefinition(
name = "Cart direct download limit",
description = "Cart size limit for direct download from AEM...",
type = AttributeType.LONG
)
long directDownloadLimit() default 52428800L; // 50MB

@AttributeDefinition(
name = "S3 Bucket Name e.g. eaem-s3-bucket",
description = "S3 Bucket Name e.g. eaem-s3-bucket",
type = AttributeType.STRING
)
String s3BucketName();
}
}


2) Add a servlet apps.experienceaem.assets.EAEMS3DownloadServlet to process direct download and cart creation requests. Servlet check if the pre zip size (of all assets combined) is less than a configurable direct download limit directDownloadLimit; if size is less than the limit, the cart is available for immediate download from modal, otherwise an email is sent to user when the cart is processed in sling queue and ready for download...

package apps.experienceaem.assets;

import com.day.cq.dam.api.Asset;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.security.user.Authorizable;
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.request.RequestParameter;
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.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.jcr.base.util.AccessControlUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component(
service = Servlet.class,
property = {
"sling.servlet.methods=GET,POST",
"sling.servlet.paths=/bin/experience-aem/cart"
}
)
public class EAEMS3DownloadServlet extends SlingAllMethodsServlet {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private final long GB_20 = 21474836480L;

@Reference
private EAEMS3Service eaems3Service;

@Reference
private JobManager jobManager;

public final void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

public final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String paths = request.getParameter("paths");

if(StringUtils.isEmpty(paths)){
RequestParameter[] pathParams = request.getRequestParameters("path");

if(ArrayUtils.isEmpty(pathParams)){
response.sendError(403, "Missing path parameters");
return;
}

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

for(RequestParameter param : pathParams){
rPaths.add(param.getString());
}

paths = StringUtils.join(rPaths, ",");
}

logger.debug("Processing download of paths - " + paths);

ResourceResolver resolver = request.getResourceResolver();

List<Asset> assets = eaems3Service.getAssets(resolver, paths);

try{
long sizeOfContents = eaems3Service.getSizeOfContents(assets);

if(sizeOfContents > GB_20 ){
response.sendError(403, "Requested content too large");
return;
}

if(sizeOfContents < eaems3Service.getDirectDownloadLimit() ){
response.sendRedirect(eaems3Service.getDirectDownloadUrl(assets));
return;
}

String userId = request.getUserPrincipal().getName();
String email = eaems3Service.getUserEmail(resolver, userId);

if(StringUtils.isEmpty(email)){
response.sendError(500, "No email address registered for user - " + userId);
return;
}

String cartName = eaems3Service.getCartZipFileName(request.getUserPrincipal().getName());

logger.debug("Creating job for cart - " + cartName + ", with assets - " + paths);

Map<String, Object> payload = new HashMap<String, Object>();

payload.put(EAEMCartCreateJobConsumer.CART_NAME, cartName);
payload.put(EAEMCartCreateJobConsumer.ASSET_PATHS, paths);
payload.put(EAEMCartCreateJobConsumer.CART_RECEIVER_EMAIL, email);

jobManager.addJob(EAEMCartCreateJobConsumer.JOB_TOPIC, payload);

response.sendRedirect(request.getHeader("referer"));
}catch(Exception e){
logger.error("Error creating cart zip", e);
response.sendError(500, "Error creating cart zip - " + e.getMessage());
}
}
}


3) Add a sling job for processing carts in an ordered fashion /apps/eaem-asc-s3-presigned-cart-urls/config/org.apache.sling.event.jobs.QueueConfiguration-eaem-cart.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
queue.maxparallel="{Long}1"
queue.name="Experience AEM Cart Creation Queue"
queue.priority="MIN"
queue.retries="{Long}1"
queue.retrydelay="{Long}5000"
queue.topics="apps/experienceaem/assets/cart"
queue.type="ORDERED"/>


4) Create the email template /apps/eaem-asc-s3-presigned-cart-urls/mail-templates/cart-template.html

Subject: ${subject}

<table style="width:100%" width="100%" bgcolor="#ffffff" style="background-color:#ffffff;" border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="width:100%"> </td>
</tr>
<tr>
<td style="width:100%">Assets in cart : ${assetNames}</td>
</tr>
<tr>
<td style="width:100%"> </td>
</tr>
<tr>
<td style="width:100%"><a href="${presignedUrl}">Click to download</a></td>
</tr>
<tr>
<td style="width:100%"> </td>
</tr>
<tr>
<td style="width:100%">Download link for copy paste in browser - ${presignedUrl}</a></td>
</tr>
</table>


5) Add a job consumer apps.experienceaem.assets.EAEMCartCreateJobConsumer to process cart requests put in queue and email user the S3 presigned url link when its ready for download...

package apps.experienceaem.assets;

import com.day.cq.dam.api.Asset;
import com.day.cq.mailer.MessageGatewayService;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.commons.mail.MailTemplate;
import com.day.cq.mailer.MessageGateway;
import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.HtmlEmail;

import javax.jcr.Session;
import javax.mail.internet.InternetAddress;
import java.util.*;

@Component(
immediate = true,
service = {JobConsumer.class},
property = {
"process.label = Experience AEM Cart Create Job Topic",
JobConsumer.PROPERTY_TOPICS + "=" + EAEMCartCreateJobConsumer.JOB_TOPIC
}
)
public class EAEMCartCreateJobConsumer implements JobConsumer {
private static final Logger log = LoggerFactory.getLogger(EAEMCartCreateJobConsumer.class);

public static final String JOB_TOPIC = "apps/experienceaem/assets/cart";
public static final String CART_NAME = "CART_NAME";
public static final String CART_RECEIVER_EMAIL = "CART_RECEIVER_EMAIL";
public static final String ASSET_PATHS = "ASSET_PATHS";
private static String EMAIL_TEMPLATE_PATH = "/apps/eaem-asc-s3-presigned-cart-urls/mail-templates/cart-template.html";

@Reference
private MessageGatewayService messageGatewayService;

@Reference
ResourceResolverFactory resourceResolverFactory;

@Reference
private EAEMS3Service eaems3Service;

@Override
public JobResult process(final Job job) {
long startTime = System.currentTimeMillis();

String cartName = (String)job.getProperty(CART_NAME);
String assetPaths = (String)job.getProperty(ASSET_PATHS);
String receiverEmail = (String)job.getProperty(CART_RECEIVER_EMAIL);

log.debug("Start processing cart - " + cartName);

ResourceResolver resolver = null;

try{
resolver = resourceResolverFactory.getAdministrativeResourceResolver(null);

List<Asset> assets = eaems3Service.getAssets(resolver, assetPaths);

String cartTempFilePath = eaems3Service.createTempZip(assets, cartName);

log.debug("Cart - " + cartName + ", creation took " + ((System.currentTimeMillis() - startTime) / 1000) + " secs");

String objectKey = eaems3Service.uploadToS3(cartName, cartTempFilePath);

String presignedUrl = eaems3Service.getS3PresignedUrl(objectKey, cartName, EAEMS3Service.ZIP_MIME_TYPE);

log.debug("Cart - " + cartName + ", with object key - " + objectKey + ", creation and upload to S3 took " + ((System.currentTimeMillis() - startTime) / 1000) + " secs");

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

for(Asset asset : assets){
assetNames.add(asset.getName());
}

log.debug("Sending email to - " + receiverEmail + ", with assetNames in cart - " + cartName + " - " + StringUtils.join(assetNames, ","));

Map<String, String> emailParams = new HashMap<String,String>();

emailParams.put("subject", "Ready for download - " + cartName);
emailParams.put("assetNames", StringUtils.join(assetNames, ","));
emailParams.put("presignedUrl", presignedUrl);

sendMail(resolver, emailParams, receiverEmail);

log.debug("End processing cart - " + cartName);
}catch(Exception e){
log.error("Error creating cart - " + cartName + ", with assets - " + assetPaths, e);
return JobResult.FAILED;
}finally{
if(resolver != null){
resolver.close();
}
}

return JobResult.OK;
}

private Email sendMail(ResourceResolver resolver, Map<String, String> emailParams, String recipientEmail) throws Exception{
MailTemplate mailTemplate = MailTemplate.create(EMAIL_TEMPLATE_PATH, resolver.adaptTo(Session.class));

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

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

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

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

messageGateway.send(email);

return email;
}
}


6) Create a component /apps/eaem-asc-s3-presigned-cart-urls/components/download with sling:resourceSuperType /apps/asset-share-commons/components/modals/download to provide the Email download link functionality



7) Create a sling model apps.experienceaem.assets.EAEMDownload for use in the download HTL script

package apps.experienceaem.assets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Required;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;

@Model(
adaptables = {SlingHttpServletRequest.class},
resourceType = {EAEMDownload.RESOURCE_TYPE}
)
public class EAEMDownload {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

protected static final String RESOURCE_TYPE = "/apps/eaem-asc-s3-presigned-cart-urls/components/download";

@Self
@Required
protected SlingHttpServletRequest request;

@OSGiService
@Required
private EAEMS3Service eaems3Service;

protected Long directDownloadLimit;

protected Long cartSize;

@PostConstruct
protected void init() {
directDownloadLimit = eaems3Service.getDirectDownloadLimit();

try{
cartSize = eaems3Service.getSizeOfContents(eaems3Service.getAssets(request.getResourceResolver(),
request.getRequestParameters("path")));
}catch (Exception e){
logger.error("Error calculating cart size", e);
}
}

public long getDirectDownloadLimit() {
return this.directDownloadLimit;
}

public long getCartSize() {
return this.cartSize;
}
}

Add the sling model package in eaem-asc-s3-presigned-cart-urls\bundle\pom.xml

<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>apps.experienceaem.assets.eaem-asc-s3-presigned-cart-urls-bundle</Bundle-SymbolicName>
<Sling-Model-Packages>
apps.experienceaem.assets
</Sling-Model-Packages>
</instructions>
</configuration>
</plugin>


8) Add the necessary changes to modal code /apps/eaem-asc-s3-presigned-cart-urls/components/download/download.html and add component in action page eg. http://localhost:4502/editor.html/content/asset-share-commons/en/light/actions/download.html

<sly data-sly-use.eaemDownload="apps.experienceaem.assets.EAEMDownload"></sly>

<form method="post"
action="/bin/experience-aem/cart"
target="download"
data-asset-share-id="download-modal"
class="ui modal cmp-modal-download--wrapper cmp-modal">

    .......................

<div data-sly-test.isCartDownload="${eaemDownload.cartSize > eaemDownload.directDownloadLimit}"
class="ui attached warning message cmp-message">
<span class="detail">Size exceeds limit for direct download, you'll receive an email when the cart is ready for download</span>
</div>

    .......................

<button type="submit" class="ui positive primary right labeled icon button ${isMaxSize ? 'disabled': ''}">
${isCartDownload ? 'Email when ready' : properties['downloadButton'] }
<i class="download icon"></i>
</button>

AEM 6520 - Sort Tags Alphabetically in Tag Move Picker

$
0
0

Goal


Sort Tags alphabetically in the Tag Move Picker

For sorting tags in console check this post

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-alpha-order-move-tags-picker

2) Create node /apps/eaem-alpha-order-move-tags-picker/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.tagging.touch.movetag], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-alpha-order-move-tags-picker/clientlib/js.txt, add

                        tags-order.js

4) Create file (nt:file) /apps/eaem-alpha-order-move-tags-picker/clientlib/tags-order.js, add the following code

(function ($, $document) {
var EAEM_SORTED = "eaem-sorted";

$document.on("foundation-contentloaded", handleMovePicker);

function handleMovePicker(){
var $cuiPathBrowser = $(".coral-PathBrowser");

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

$cuiPathBrowser.each(extendPicker);
}

function extendPicker(index, cuiPathBrowser){
cuiPathBrowser = $(cuiPathBrowser).data("pathBrowser");

var cuiPicker = cuiPathBrowser.$picker.data("picker");

cuiPathBrowser.$button.on("click", function() {
setTimeout(function(){
if(!cuiPicker.columnView){
console.log("EAEM - could not initialize column view");
return;
}

extendColumnView(cuiPicker.columnView);
}, 200);
});
}

function extendColumnView(columnView){
function alphaSortHandler(event, href){
if(_.isEmpty(href)){
return;
}

var columnData = columnView._data[href];

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

var $columnData = $(columnData);

alphaSort($columnData);

columnView._data[href] = $columnData[0].outerHTML;
}

function alphaSort($columnData){
var $cContent = $columnData.find(".coral-ColumnView-column-content"),
$items = $cContent.find(".coral-ColumnView-item");

if(!_.isEmpty($cContent.data(EAEM_SORTED))){
return;
}

$items.sort(function(a, b) {
var aTitle = a.getAttribute("title"),
bTitle = b.getAttribute("title");

return (bTitle.toUpperCase()) < (aTitle.toUpperCase()) ? 1 : -1;
});

$cContent.data(EAEM_SORTED, "true");

$items.detach().appendTo($cContent);
}

columnView.$element.on('coral-columnview-load', alphaSortHandler);

alphaSort(columnView.$element);
}

}(jQuery, jQuery(document)));

AEM 6460 - Force open a specific metadata schema while editing assets in bulk

$
0
0

Goal


While bulk editing assets (eg. by selecting random assets in search results) if the usecase is to force load a specific schema, try the following sling filter...

In the following example, opening metadata editor http://localhost:4502/mnt/overlay/dam/gui/content/assets/metadataeditor.external.html by selecting multiple assets always shows the schema experience-aem-schema

Solution


Create a Sling filter with the following code....

package com.experienceaem.assets;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

@Component(
service = Filter.class,
immediate = true,
name = "Force the metadata schema editor to load Experience AEM Schema",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=^(\\/mnt\\/overlay\\/dam\\/gui\\/content\\/assets\\/metadataeditor).*$"
}
)
public class ForceMetadataSchemaFilter implements Filter {

private final String FORCED_FORM_TYPE_NAME = "forcedFormTypeName";

private final String EXPERIENCE_AEM_SCHEMA = "/experience-aem-schema";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new MetadataSchemaRequestWrapper((SlingHttpServletRequest) request), response);
}

@Override
public void destroy() {
}

private class MetadataSchemaRequestWrapper extends SlingHttpServletRequestWrapper {
public MetadataSchemaRequestWrapper(final SlingHttpServletRequest request) {
super(request);
}

@Override
public RequestParameter getRequestParameter(String paramName) {
RequestParameter requestParam = super.getRequestParameter(paramName);

if(!FORCED_FORM_TYPE_NAME.equals(paramName)){
return requestParam;
}

String[] content = (String[])super.getAttribute("aem.assets.ui.properties.content");

if(ArrayUtils.isEmpty(content) || (content.length == 1)){
return requestParam;
}

requestParam = new RequestParameter() {
@Override
public String getName() {
return FORCED_FORM_TYPE_NAME;
}

@Override
public boolean isFormField() {
return true;
}

@Override
public String getContentType() {
return null;
}

@Override
public long getSize() {
return 0;
}

@Override
public byte[] get() {
return new byte[0];
}

@Override
public InputStream getInputStream() throws IOException {
return null;
}

@Override
public String getFileName() {
return null;
}

@Override
public String getString() {
return EXPERIENCE_AEM_SCHEMA;
}

@Override
public String getString(String s) throws UnsupportedEncodingException {
return EXPERIENCE_AEM_SCHEMA;
}

@Override
public String toString() {
return EXPERIENCE_AEM_SCHEMA;
}
};

return requestParam;
}
}
}

AEM 6460 - Query Builder Predicate Evaluator for searching assets by metadata property ignore case

$
0
0

Goal


Add a Query Builder predicate evaluator to search for assets by Metadata property ignoring case...

Query generated by this code is something like...

/jcr:root/content/dam/experience-aem//element(*, dam:Asset)[jcr:contains(jcr:content/metadata/@eaem:assetDescription,  'vIdEo')] 
order by jcr:content/@jcr:lastModified descending

Avoid using fn:lower-case() ; it might result in slowness depending on repo size...

/jcr:root/content/dam/experience-aem//element(*, dam:Asset)[((fn:lower-case(jcr:content/metadata/@eaem:assetDescription) =  'video'))] 
order by jcr:content/@jcr:lastModified descending


Solution


1) Create a Predicate evaluator IgnoreCaseJcrPropertyPredicateEvaluator extending com.day.cq.search.eval.JcrPropertyPredicateEvaluator

package com.experienceaem.assets;

import com.day.cq.search.Predicate;
import com.day.cq.search.eval.EvaluationContext;
import com.day.cq.search.eval.JcrPropertyPredicateEvaluator;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;
import com.day.cq.search.eval.XPath;
@Component(
factory = "com.day.cq.search.eval.PredicateEvaluator/property"
)
public class IgnoreCaseJcrPropertyPredicateEvaluator extends JcrPropertyPredicateEvaluator {
private String JCR_METADATA_PREFIX = "(jcr:content/metadata/";

public String getXPathExpression(Predicate p, EvaluationContext context) {
String xPathExpr = super.getXPathExpression(p, context);

if(StringUtils.isEmpty(xPathExpr)){
return xPathExpr;
}

if(xPathExpr.startsWith(JCR_METADATA_PREFIX)){
String value = xPathExpr.substring(xPathExpr.indexOf("=") + 1);

value = value.toLowerCase();
xPathExpr = "jcr:contains" + xPathExpr.substring(0, xPathExpr.indexOf("=")) + "," + value;
}

return xPathExpr;
}
}

AEM 6530 - Touch UI Assets Console add Metadata while Uploading Files

$
0
0

Goal


Show simple Metadata Form in the Upload Dialog of Touch UI Assets Console; With this extension users can add metadata while uploading files

For AEM 63 check this post

Demo | Package Install | Github


Metadata Form in Upload Dialog



Metadata Validation



Metadata in Properties



Metadata in CRX



Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-assets-file-upload-with-metadata

2) Metadata form shown in Upload Dialog is an Authoring Dialog; Create the dialog /apps/eaem-assets-file-upload-with-metadata/dialog, add metadata form nodes



3) Create node /apps/eaem-assets-file-upload-with-metadata/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value dam.gui.coral.fileupload String[] property dependencies with value underscore

4) Create file (nt:file) /apps/eaem-assets-file-upload-with-metadata/clientlib/js.txt, add

            fileupload-with-metadata.js

5) Create file (nt:file) /apps/eaem-assets-file-upload-with-metadata/clientlib/fileupload-with-metadata.js, add the following code

(function($, $document) {
var METADATA_DIALOG = "/apps/eaem-assets-file-upload-with-metadata/dialog.html",
METADATA_PREFIX = "eaem",
UPLOAD_LIST_DIALOG = ".uploadListDialog.is-open",
ACTION_CHECK_DATA_VALIDITY = "ACTION_CHECK_DATA_VALIDITY",
ACTION_POST_METADATA = "ACTION_POST_METADATA",
dialogAdded = false;
url = document.location.pathname;

if( url.indexOf("/assets.html") == 0 ){
handleAssetsConsole();
}else if(url.indexOf(METADATA_DIALOG) == 0){
handleMetadataDialog();
}

function handleAssetsConsole(){
$document.on("foundation-contentloaded", handleFileAdditions);
}

function handleMetadataDialog(){
$(function(){
_.defer(styleMetadataIframe);
});
}

function registerReceiveDataListener(handler) {
if (window.addEventListener) {
window.addEventListener("message", handler, false);
} else if (window.attachEvent) {
window.attachEvent("onmessage", handler);
}
}

function styleMetadataIframe(){
var $dialog = $("coral-dialog");

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

$dialog.css("overflow", "hidden");

$dialog[0].open = true;

var $header = $dialog.css("background-color", "#fff").find(".coral3-Dialog-header");

$header.find(".cq-dialog-actions").remove();

$dialog.find(".coral3-Dialog-wrapper").css("margin","0").find(".coral-Dialog-content").css("padding","0");

registerReceiveDataListener(postMetadata);

function postMetadata(event){
var message = JSON.parse(event.data);

if( message.action !== ACTION_CHECK_DATA_VALIDITY ){
return;
}

var $dialog = $(".cq-dialog"),
$fields = $dialog.find("[name^='./']"),
data = {}, $field, $fValidation, name, value, values,
isDataInValid = false;

$fields.each(function(index, field){
$field = $(field);
name = $field.attr("name");
value = $field.val();

$fValidation = $field.adaptTo("foundation-validation");

if($fValidation && !$fValidation.checkValidity()){
isDataInValid = true;
}

$field.updateErrorUI();

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

name = name.substr(2);

if(!name.indexOf(METADATA_PREFIX) === 0){
return
}

if(!_.isEmpty(data[name])){
if(_.isArray(data[name])){
data[name].push(value);
}else{
values = [];
values.push(data[name]);
values.push(value);

data[name] = values;
data[name + "@TypeHint"] = "String[]";
}
}else{
data[name] = value;
}
});

sendValidityMessage(isDataInValid, data);
}

function sendValidityMessage(isDataInValid, data){
var message = {
action: ACTION_CHECK_DATA_VALIDITY,
data: data,
isDataInValid: isDataInValid
};

parent.postMessage(JSON.stringify(message), "*");
}
}

function handleFileAdditions(){
var $fileUpload = $("dam-chunkfileupload"),
$metadataIFrame, $uploadButton, validateUploadButton,
metadata;

$fileUpload.on('change', addMetadataDialog);

$fileUpload.on('dam-fileupload:loadend', postMetadata);

function sendDataMessage(message){
$metadataIFrame[0].contentWindow.postMessage(JSON.stringify(message), "*");
}

function addMetadataDialog(){
if(dialogAdded){
return;
}

dialogAdded = true;

_.debounce(addDialog, 500)();
}

function addDialog(){
var $dialog = $(UPLOAD_LIST_DIALOG);

if(!_.isEmpty($dialog.find("iframe"))){
$dialog.find("iframe").remove();
}

var iFrame = '<iframe width="550px" height="450px" frameborder="0" src="' + METADATA_DIALOG + '"/>',
$dialogContent = $dialog.find("coral-dialog-content");

$metadataIFrame = $(iFrame).appendTo($dialogContent.css("max-height", "600px"));
$dialogContent.find("input").css("width", "30rem");
$dialogContent.closest(".coral3-Dialog-wrapper").css("top", "30%").css("left", "50%");

addValidateUploadButton($dialog);
}

function addValidateUploadButton($dialog){
var $footer = $dialog.find("coral-dialog-footer");

$uploadButton = $footer.find("coral-button-label:contains('Upload')").closest("button");

validateUploadButton = new Coral.Button().set({
variant: 'primary'
});

validateUploadButton.label.textContent = Granite.I18n.get('Upload');

validateUploadButton.classList.add('dam-asset-upload-button');

$footer[0].appendChild(validateUploadButton);

$uploadButton.hide();

validateUploadButton.hide();

validateUploadButton.on('click', function() {
checkDataValidity();
});

$metadataIFrame.on('load', function(){
validateUploadButton.show();
dialogAdded = false;
});

registerReceiveDataListener(isMetadataValid);
}

function isMetadataValid(event){
var message = JSON.parse(event.data);

if( (message.action !== ACTION_CHECK_DATA_VALIDITY)){
return;
}

if(message.isDataInValid){
return;
}

metadata = message.data;

validateUploadButton.hide();

$uploadButton.click();
}

function checkDataValidity(){
var message = {
action: ACTION_CHECK_DATA_VALIDITY
};

sendDataMessage(message);
}

function postMetadata(event){
var detail = event.originalEvent.detail,
folderPath = detail.action.replace(".createasset.html", ""),
assetMetadataPath = folderPath + "/" + detail.item.name + "/jcr:content/metadata";

//jcr:content/metadata created by the DAM Update asset workflow may not be available when the below post
//call executes; ideally, post the parameters to aem, a scheduler runs and adds the metadata when metadata
//node becomes available
$.ajax({
type : 'POST',
url : assetMetadataPath,
data : metadata
})
}
}

})(jQuery, jQuery(document));

6) #223 is for demo purposes only; in real world implementations, the metadata node created by DAM Update Asset workflow may not exist yet, when the ajax post metadata call executes. Replace this direct call with a servlet temporarily storing the form metadata and update the metadata node (when it becomes available) in a scheduler or listener
Viewing all 526 articles
Browse latest View live


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