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

AEM Cloud Service - CSS and Color Picker Plugin for Rich Text Editor (RTE)

$
0
0

Goal

AEM Cloud Version :  2021.3.5026.20210309T210727Z-210225 (March 09, 2021)

This post explains adding a plugin to RTE (Rich Text Editor - /libs/cq/gui/components/authoring/dialog/richtext) for adding custom CSS and color to text...

Package install contains a Text component with design dialog with plugin configuration - /apps/eaem-cs-rte-plugin-color-picker/components/text/cq:design_dialog/content/items/tabs/items/plugins/items/eaem-aem-fonts

Demo | Package Install | Github


Plugin Picker 


Applying Color 


Design Dialog Configuration


Enable Plugin Component Policy


Plugin Config saved in Component Policy


Solution

1) Create the Picker configuration dialog /apps/eaem-cs-rte-plugin-color-picker/clientlibs/fonts-plugin/font-selector and add categories=eaem.rte.font.plugin in /apps/eaem-cs-rte-plugin-color-picker/clientlibs/fonts-plugin/font-selector/jcr:content/head/clientlibs

<?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 Font Selector"
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, eaem.rte.font.plugin]"/>
</head>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/body">
<items jcr:primaryType="nt:unstructured">
<form
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form"
class="foundation-form content-container"
maximized="{Boolean}true"
style="vertical">
<items jcr:primaryType="nt:unstructured">
<wizard
jcr:primaryType="nt:unstructured"
jcr:title="Select Text Font Color..."
sling:resourceType="granite/ui/components/coral/foundation/wizard">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
jcr:title="Select Text Font Color..."
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<fixedColumns
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">
<contents
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldDescription="Appply specific background style..."
fieldLabel="Background Style"
name="./style">
<items jcr:primaryType="nt:unstructured">
<none
jcr:primaryType="nt:unstructured"
text="None"
value=""/>
<gray
jcr:primaryType="nt:unstructured"
text="Gray"
value="eaem--background-gray"/>
<white
jcr:primaryType="nt:unstructured"
text="White"
value="eaem--background-white"/>
<black
jcr:primaryType="nt:unstructured"
text="Black"
value="eaem--background-black"/>
</items>
</contents>
<hideOnDesktop
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldDescription="Hide the text on desktop ( >= 992px)"
name="./hideOnDesktop"
text="Hide on desktop"
value="true"/>
<hideOnTablet
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldDescription="Hide the text on tablet (768 - 991.95 px)"
name="./hideOnTablet"
text="Hide on Tablet"
value="true"/>
<hideOnMobile
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
fieldDescription="Hide the text on mobile breakpoints (< 768px)"
name="./hideOnMobile"
text="Hide on mobile"
value="true"/>
<color
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/colorfield"
fieldDescription="Select text color"
fieldLabel="Text Color"
name="./color"
showProperties="{Boolean}true"/>
<bgColor
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/colorfield"
fieldDescription="Select background color"
fieldLabel="Background Color"
name="./bgColor"
showProperties="{Boolean}true"/>
</items>
</column>
</items>
</fixedColumns>
</items>
<parentConfig jcr:primaryType="nt:unstructured">
<prev
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
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"
disabled="{Boolean}true"
text="Apply"
type="submit"
variant="primary">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="next"/>
</next>
</parentConfig>
</text>
</items>
</wizard>
</items>
</form>
</items>
</body>
</jcr:content>
</jcr:root>


2) Add any custom styles for the author user to (pick and apply on text using the plugin) in clientlib base eg. /apps/eaem-cs-rte-plugin-color-picker/clientlibs/clientlib-base/rte-fonts.css

.eaem--background-gray{
background-color: #f1f1f1;
}

.eaem--background-white{
background-color: #ffffff;
}

.eaem--background-black{
background-color: #000000;
}

@media (min-width: 992px){
.eaem--content-desktop-hide {
display: none !important;
}
}

@media (min-width:768px) and (max-width:991.95px){
.eaem--content-tablet-hide {
display: none !important;
}
}

@media (max-width: 767.95px){
.eaem--content-mobile-hide {
display: none !important;
}
}


3) Add the plugin execution logic in /apps/eaem-cs-rte-plugin-color-picker/clientlibs/fonts-plugin/clientlib/rte-fonts-plugin.js with categories=[cq.authoring.dialog.all, eaem.rte.font.plugin]

(function($, CUI, $document){
var GROUP = "eaem-aem-fonts",
FONT_FEATURE = "applyFont",
TEXT_COLOR_FEATURE = "textColor",
TEXT_BG_COLOR_FEATURE = "textBackgroundColor",
EAEM_APPLY_FONT_DIALOG = "eaemTouchUIApplyFontDialog",
SENDER = "eaem-aem", REQUESTER = "requester", $eaemFontPicker,
CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
FONT_SELECTOR_URL = "/apps/eaem-cs-rte-plugin-color-picker/clientlibs/fonts-plugin/font-selector.html",
MOBILE_HIDE_CONTENT_CLASS = "eaem--content-mobile-hide",
DESKTOP_HIDE_CONTENT_CLASS = "eaem--content-desktop-hide",
TABLET_HIDE_CONTENT_CLASS = "eaem--content-tablet-hide",
url = document.location.pathname;

if( url.indexOf(FONT_SELECTOR_URL) == 0 ){
handlePicker();
return;
}

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

$document.on("click", CANCEL_CSS, sendCancelMessage);

$document.submit(sentTextAttributes);
}

function queryParameters() {
var result = {}, param,
params = document.location.search.split(/\?|\&/);

params.forEach( function(it) {
if (_.isEmpty(it)) {
return;
}

param = it.split("=");
result[param[0]] = param[1];
});

return result;
}

function setWidgetValue(form, selector, value, enable){
Coral.commons.ready(form.querySelector(selector), function (field) {
if(field.tagName == "CORAL-CHECKBOX"){
if(value == "true"){
field.checked = true;
}
}else{
field.value = _.isEmpty(value) ? "" : decodeURIComponent(value);
}

if(enable){
delete field.disabled;
}else{
field.disabled = "disabled";
}
});
}

function fillDefaultValues(){
var queryParams = queryParameters(),
$form = $("form");

if(_.isEmpty(queryParams.features)){
return;
}

var features = queryParams.features.split(",");

setWidgetValue($form[0], "[name='./style']", queryParams.class, true);

setWidgetValue($form[0], "[name='./hideOnMobile']", queryParams.hideOnMobile, true);

setWidgetValue($form[0], "[name='./hideOnTablet']", queryParams.hideOnTablet, true);

setWidgetValue($form[0], "[name='./hideOnDesktop']", queryParams.hideOnDesktop, true);

setWidgetValue($form[0], "[name='./color']", queryParams.color, features.includes(TEXT_COLOR_FEATURE));

setWidgetValue($form[0], "[name='./bgColor']", queryParams.bgColor, features.includes(TEXT_BG_COLOR_FEATURE));

$form.css("background-color", "#fff");
}

function sentTextAttributes(){
var message = {
sender: SENDER,
action: "submit",
data: {}
}, $form = $("form"), $field;

_.each($form.find("[name^='./']"), function(field){
$field = $(field);
message.data[$field.attr("name").substr(2)] = $field.val();
});

addCheckboxValue(message, $form, "./hideOnDesktop");

addCheckboxValue(message, $form, "./hideOnTablet");

addCheckboxValue(message, $form, "./hideOnMobile");

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

function addCheckboxValue(message, $form, cbName){
var $checkbox = $form.find("coral-checkbox[name='" + cbName + "']");

if(!_.isEmpty($checkbox)){
message.data[$checkbox.attr("name").substr(2)] = $checkbox[0].checked;
}
}

function sendCancelMessage(){
var message = {
sender: SENDER,
action: "cancel"
};

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

function getParent() {
if (window.opener) {
return window.opener;
}

return parent;
}

addPlugin();

addPluginToDefaultUISettings();

addDialogTemplate();

function addDialogTemplate(){
var url = Granite.HTTP.externalize(FONT_SELECTOR_URL) + "?" + REQUESTER + "=" + SENDER;

var html = "<iframe width='600px' height='450px' frameBorder='0' src='" + url + "'></iframe>";

if(_.isUndefined(CUI.rte.Templates)){
CUI.rte.Templates = {};
}

if(_.isUndefined(CUI.rte.templates)){
CUI.rte.templates = {};
}

try{
CUI.rte.templates['dlg-' + EAEM_APPLY_FONT_DIALOG] = CUI.rte.Templates['dlg-' + EAEM_APPLY_FONT_DIALOG] = Handlebars.compile(html);
}catch(err){
console.log("Ignoring font plugin error", err);
}
}

function rgbToHex(color){
if(_.isEmpty(color)){
return color;
}

if(color.indexOf("rgb") == 0){
color = CUI.util.color.RGBAToHex(color);
}

return color;
}

function addPluginToDefaultUISettings(){
var groupFeature = GROUP + "#" + FONT_FEATURE,
toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.dialogFullScreen.toolbar;

if(toolbar.includes(groupFeature)){
return;
}

toolbar.splice(3, 0, groupFeature);
}

var EAEMApplyFontDialog = new Class({
extend: CUI.rte.ui.cui.AbstractDialog,

toString: "EAEMApplyFontDialog",

initialize: function(config) {
this.exec = config.execute;
},

getDataType: function() {
return EAEM_APPLY_FONT_DIALOG;
}
});

function addPlugin(){
var EAEMTouchUIFontPlugin = new Class({
toString: "EAEMTouchUIFontPlugin",

extend: CUI.rte.plugins.Plugin,

pickerUI: null,

getFeatures: function() {
return [ FONT_FEATURE ];
},

initializeUI: function(tbGenerator) {
var plg = CUI.rte.plugins;

addPluginToDefaultUISettings();

if (!this.isFeatureEnabled(FONT_FEATURE)) {
return;
}

this.pickerUI = tbGenerator.createElement(FONT_FEATURE, this, false, { title: "Select Font..." });
tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 10);

var groupFeature = GROUP + "#" + FONT_FEATURE;
tbGenerator.registerIcon(groupFeature, "colorPalette");
},

notifyPluginConfig: function (pluginConfig) {
pluginConfig = pluginConfig || {};

CUI.rte.Utils.applyDefaults(pluginConfig, {
'tooltips': {
applyFont: {
'title': 'Apply Font',
'text': 'Apply Font to selected text'
}
}
});

this.config = pluginConfig;
},

execute: function (pluginCommand, value, envOptions) {
var context = envOptions.editContext,
ek = this.editorKernel;

if (pluginCommand != FONT_FEATURE) {
return;
}

if(!isValidSelection()){
return;
}

var selection = CUI.rte.Selection.createProcessingSelection(context),
startNode = selection.startNode;

if ( (selection.startOffset === startNode.length) && (startNode != selection.endNode)) {
startNode = startNode.nextSibling;
}

var $tag = $(CUI.rte.Common.getTagInPath(context, startNode, "span")),
clazz = $tag.attr("class"), hideOnMobile = false, hideOnDesktop = false, hideOnTablet = false,
size = $tag.css("font-size"),dialog,dm = ek.getDialogManager(),
$container = CUI.rte.UIUtils.getUIContainer($(context.root)),
propConfig = {
'parameters': {
'command': this.pluginId + '#' + FONT_FEATURE
}
};

if(clazz && clazz.includes(MOBILE_HIDE_CONTENT_CLASS)){
hideOnMobile = true;
clazz = clazz.replace(MOBILE_HIDE_CONTENT_CLASS, "").trim();
}

if(clazz && clazz.includes(TABLET_HIDE_CONTENT_CLASS)){
hideOnTablet = true;
clazz = clazz.replace(TABLET_HIDE_CONTENT_CLASS, "").trim();
}

if(clazz && clazz.includes(DESKTOP_HIDE_CONTENT_CLASS)){
hideOnDesktop = true;
clazz = clazz.replace(DESKTOP_HIDE_CONTENT_CLASS, "").trim();
}

var color = this.getColorAttributes($tag);

if(this.eaemApplyFontDialog){
dialog = this.eaemApplyFontDialog;

dialog.$dialog.find("iframe").attr("src", this.getPickerIFrameUrl(this.config.features, size, clazz,
hideOnMobile, hideOnTablet, hideOnDesktop, color.color, color.bgColor));
}else{
dialog = new EAEMApplyFontDialog();

dialog.attach(propConfig, $container, this.editorKernel);

dialog.$dialog.css("-webkit-transform", "scale(0.9)").css("-webkit-transform-origin", "0 0")
.css("-moz-transform", "scale(0.9)").css("-moz-transform-origin", "0px 0px");

dialog.$dialog.find("iframe").attr("src",
this.getPickerIFrameUrl(this.config.features, size, clazz, hideOnMobile, hideOnTablet, hideOnDesktop, color.color, color.bgColor));

this.eaemApplyFontDialog = dialog;
}

dm.show(dialog);

$(window).off('message', receiveMessage).on('message', receiveMessage);

function isValidSelection(){
var winSel = window.getSelection();
return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;
}

function receiveMessage(event) {
event = event.originalEvent || {};

if (_.isEmpty(event.data)) {
return;
}

var message, action;

try{
message = JSON.parse(event.data);
}catch(err){
return;
}

if (!message || message.sender !== SENDER) {
return;
}

action = message.action;

if(action === "submit"){
ek.relayCmd(pluginCommand, message.data);
}

dialog.hide();
}
},

getColorAttributes: function($tag){
var key, color = { color: "", bgColor : ""};

if(!$tag.attr("style")){
return color;
}

//donot use .css("color"), it returns default font color, if color is not set
var parts = $tag.attr("style").split(";");

_.each(parts, function(value){
value = value.split(":");

key = value[0] ? value[0].trim() : "";
value = value[1] ? value[1].trim() : "";

if(key == "color"){
color.color = rgbToHex(value);
}else if(key == "background-color"){
color.bgColor = rgbToHex(value);
}
});

return color;
},

showFontModal: function(url){
var self = this, $iframe = $('<iframe>'),
$modal = $('<div>').addClass('eaem-cfm-font-size coral-Modal');

$iframe.attr('src', url).appendTo($modal);

$modal.appendTo('body').modal({
type: 'default',
buttons: [],
visible: true
});

$eaemFontPicker = $modal;

$eaemFontPicker.eaemFontPlugin = self;

$modal.nextAll(".coral-Modal-backdrop").addClass("cfm-coral2-backdrop");
},

getPickerIFrameUrl: function(features, size, clazz, hideOnMobile, hideOnTablet, hideOnDesktop, color, bgColor){
var url = Granite.HTTP.externalize(FONT_SELECTOR_URL) + "?" + REQUESTER + "=" + SENDER;

if(features === "*"){
features = [TEXT_COLOR_FEATURE , TEXT_BG_COLOR_FEATURE];
}

url = url + "&features=" + features.join(",");

if(!_.isEmpty(color)){
url = url + "&color=" + encodeURIComponent(color);
}

if(!_.isEmpty(bgColor)){
url = url + "&bgColor=" + encodeURIComponent(bgColor);
}

if(!_.isEmpty(size)){
url = url + "&size=" + size;
}

if(!_.isEmpty(clazz)){
url = url + "&class=" + clazz;
}

if(hideOnMobile){
url = url + "&hideOnMobile=" + hideOnMobile;
}

if(hideOnTablet){
url = url + "&hideOnTablet=" + hideOnTablet;
}

if(hideOnDesktop){
url = url + "&hideOnDesktop=" + hideOnDesktop;
}

return url;
},

updateState: function(selDef) {
var hasUC = this.editorKernel.queryState(FONT_FEATURE, selDef);

if (this.pickerUI != null) {
this.pickerUI.setSelected(hasUC);
}
}
});

var EAEMTouchUIFontCmd = new Class({
toString: "EAEMTouchUIFontCmd",

extend: CUI.rte.commands.Command,

isCommand: function (cmdStr) {
return (cmdStr.toLowerCase() == FONT_FEATURE);
},

getProcessingOptions: function () {
var cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},

getTagObject: function(textData) {
var style = "";

if(!_.isEmpty(textData.color)){
style = "color: " + textData.color + ";";
}

if(!_.isEmpty(textData.size)){
style = style + "font-size: " + textData.size + ";";
}

if(!_.isEmpty(textData.bgColor)){
style = style + "background-color: " + textData.bgColor;
}

var spanTag = {
"tag": "span",
"attributes": {
"style" : style
}
};

var clazz = textData.style;

if(!_.isEmpty(clazz)){
spanTag.attributes.class = clazz;
}

if(textData.hideOnMobile){
addClazz(spanTag, MOBILE_HIDE_CONTENT_CLASS);
}

if(textData.hideOnTablet){
addClazz(spanTag, TABLET_HIDE_CONTENT_CLASS);
}

if(textData.hideOnDesktop){
addClazz(spanTag, DESKTOP_HIDE_CONTENT_CLASS);
}

return spanTag;

function addClazz(tag, tagClazz){
tag.attributes.class = tag.attributes.class ? (tag.attributes.class + "") : "";
tag.attributes.class = tag.attributes.class + tagClazz;
}
},

execute: function (execDef) {
var textData = execDef.value, selection = execDef.selection,
nodeList = execDef.nodeList;

if (!selection || !nodeList) {
return;
}

var common = CUI.rte.Common,
context = execDef.editContext,
tagObj = this.getTagObject(textData);

if(_.isEmpty(textData.size) && _.isEmpty(textData.color)
&& _.isEmpty(textData.bgColor) && _.isEmpty(textData.style)
&& !textData.hideOnMobile && !textData.hideOnDesktop && !textData.hideOnTablet){
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
return;
}

var tags = common.getTagInPath(context, selection.startNode, tagObj.tag);

//remove existing color before adding new color
if (tags != null) {
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, tags.attributes ? tags.attributes : undefined, true);
}

nodeList.surround(execDef.editContext, tagObj.tag, tagObj.attributes);
},

queryState: function(selectionDef, cmd) {
return false;
}
});

CUI.rte.commands.CommandRegistry.register(FONT_FEATURE, EAEMTouchUIFontCmd);

CUI.rte.plugins.PluginRegistry.register(GROUP,EAEMTouchUIFontPlugin);
}
}(jQuery, window.CUI,jQuery(document)));



AEM Cloud Service - Sites SPA add additional page properties in sling model response jackson export

$
0
0

Goal

AEM Cloud Version :  2021.3.5087.20210322T071003Z-210225 (March 22, 2021)

The default sling model exporter com.adobe.aem.spa.project.core.internal.impl.PageImpl for page resource types with super type spa-project-core/components/page does not add custom or additional properties on the page content node like vanity path to the response. You can always create a project specific model implementation for your pages extending com.adobe.aem.spa.project.core.models.Page, however for adding just few additional properties a simple filter might be an easier solution...

Github



Solution

Add a filter apps.experienceaem.sites.core.filters.EAEMExtendPageModelFilter with filter pattern sling.filter.pattern=.*.model.json to intercept model requests, capture the response and add additional properties (eg. sling:vanityPath) to the response reading it from cq:PageContent node

package apps.experienceaem.sites.core.filters;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;

@Component(
service = Filter.class,
immediate = true,
name = "Expereince AEM Sling Model Response Modifier Servlet Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=.*.model.json"
}
)
public class EAEMExtendPageModelFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(EAEMExtendPageModelFilter.class);

private static final String SLING_VANITYPATH = "sling:vanityPath";
private static final String SLING_VANITYPATH_JSON_PROP = "eaemVanityPath";
private static final String CHILDREN = ":children";

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

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

String uri = slingRequest.getRequestURI();

if(!uri.endsWith(".model.json")){
chain.doFilter(request, response);
return;
}

SlingHttpServletResponse modelResponse = getModelResponse((SlingHttpServletResponse) response);

chain.doFilter(slingRequest, modelResponse);

PrintWriter responseWriter = response.getWriter();

responseWriter.write(getModifiedContent(modelResponse.toString(), slingRequest));
}catch(ServletException e){
throw new ServletException("Error at EAEMExtendPageModelFilter.this.doFilter()");
}
}

SlingHttpServletResponse getModelResponse(SlingHttpServletResponse response) {
SlingHttpServletResponse modelResponse = new DefaultSlingModelResponseWrapper(response);
return modelResponse;
}

private String getModifiedContent(String origContent, SlingHttpServletRequest slingRequest){
String modifiedContent = origContent;

try{
JSONObject model = new JSONObject(origContent);

addAddnPropertiesInPageModel(model, slingRequest);

modifiedContent = model.toString();
}catch(Exception e){
log.error("Error at EAEMExtendPageModelFilter.this.getModifiedContent(origContent={}) {}", origContent, e);
modifiedContent = origContent;
}

return modifiedContent;
}

private void addAddnPropertiesInPageModel(JSONObject model, SlingHttpServletRequest slingRequest) throws Exception{
if(!model.has(CHILDREN)){
return;
}

JSONObject childrenModel = model.getJSONObject(CHILDREN);
Iterator<String> childrenItr = childrenModel.keys();
ResourceResolver resolver = slingRequest.getResourceResolver();
Resource pageContent;

while(childrenItr.hasNext()) {
String key = childrenItr.next();
JSONObject childData = childrenModel.getJSONObject(key);

pageContent = resolver.getResource(key + "/jcr:content");

if(pageContent == null){
continue;
}

ValueMap vm = pageContent.getValueMap();

String slingVanityPath = vm.get(SLING_VANITYPATH, "");

if(StringUtils.isNotEmpty(slingVanityPath)){
childData.put(SLING_VANITYPATH_JSON_PROP, slingVanityPath);
}
}
}

@Override
public void destroy() {
}

private class DefaultSlingModelResponseWrapper extends SlingHttpServletResponseWrapper {
private CharArrayWriter writer;

public DefaultSlingModelResponseWrapper (final SlingHttpServletResponse response) {
super(response);
writer = new CharArrayWriter();
}

public PrintWriter getWriter() throws IOException {
return new PrintWriter(writer);
}

public String toString() {
return writer.toString();
}
}
}



AEM Cloud Service - Dynamic Media open, download Smart Crops, Video Encodes from Preview Server (Test Context)

$
0
0


Goal

AEM Cloud Version :  2021.3.5087.20210322T071003Z-210225 (March 22, 2021)

Add a button Smart Crop Download in Asset Details action bar to open/download smart crops for images and encodes for videos. Please stress test the logic for large video encode downloads....

Demo | Package Install | Github


Image Smart Crop Download


Video Encode Download



Solution

1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config

scripts=[
"
create service user eaem-service-user with path system/cq:services/experience-aem
set principal ACL for eaem-service-user
allow jcr:read on /apps
allow jcr:all on /conf
end
"
]


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.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"
user.mapping="[eaem-cs-smart-crop-open.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a proxy servlet apps.experienceaem.assets.core.servlets.DynamicRenditionProxy to download the video encodes

package apps.experienceaem.assets.core.servlets;

import apps.experienceaem.assets.core.services.EAEMDMService;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.mime.MimeTypeService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Component(
name = "Experience AEM Dynamic Rendition Proxy Servlet",
immediate = true,
service = Servlet.class,
property = { "sling.servlet.methods=GET", "sling.servlet.paths=/bin/eaem/proxy" })
public class DynamicRenditionProxy extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(DynamicRenditionProxy.class);

@Reference
private transient HttpClientBuilderFactory httpClientBuilderFactory;

private transient CloseableHttpClient httpClient;

@Reference
private transient EAEMDMService dmcService;

@Reference
private transient ResourceResolverFactory factory;

@Reference
private transient MimeTypeService mimeTypeService;

protected void activate(final ComponentContext ctx) {
final HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();

final RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(30000)
.build();

builder.setDefaultRequestConfig(requestConfig);

httpClient = builder.build();
}

@Override
protected final void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
throws ServletException, IOException {
try {
final String drUrl = request.getParameter("dr");

if (StringUtils.isEmpty(drUrl)) {
response.getWriter().print(getAEMIPAddress());
return;
}

downloadImage(response, drUrl);
} catch (final Exception e) {
log.error("Could not get response", e);
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

private String getAEMIPAddress() throws Exception {
return Request.Get("https://ifconfig.me/ip").execute().returnContent().asString();
}

private void downloadImage(final SlingHttpServletResponse response, final String url) throws Exception {
String fileName = url.substring(url.lastIndexOf("/") + 1);
final String finalUrl = url.substring(0, url.lastIndexOf("/")) + "/"
+ URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
fileName = fileName.replaceAll(":", "-");

log.info("Encoded URL: {}", finalUrl);

final HttpGet get = new HttpGet(finalUrl);
final CloseableHttpResponse s7Response = httpClient.execute(get);

final String contentType = ContentType.get(s7Response.getEntity()).getMimeType();
fileName = fileName + "." + mimeTypeService.getExtension(contentType);

response.setContentType("application/octet-stream");
response.setHeader("Content-disposition", "attachment; filename=" + fileName);

final InputStream in = s7Response.getEntity().getContent();

final OutputStream out = response.getOutputStream();

IOUtils.copy(in, out);

out.close();

in.close();
}

private void streamImage(final SlingHttpServletResponse response, final String url) throws Exception {
response.setContentType("image/jpeg");

final byte[] image = Request.Get(url).execute().returnContent().asBytes();

final InputStream in = new ByteArrayInputStream(image);

final OutputStream out = response.getOutputStream();

IOUtils.copy(in, out);

out.close();

in.close();
}
}


4) Add a service implementation apps.experienceaem.assets.core.services.impl.EAEMDMServiceImpl for executing the S7 API and get preview server / test context url...

package apps.experienceaem.assets.core.services.impl;

import apps.experienceaem.assets.core.services.EAEMDMService;
import com.day.cq.dam.scene7.api.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
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 org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.*;
import java.util.HashMap;
import java.util.Map;


@Component(service = EAEMDMService.class)
@Designate(ocd = EAEMDMServiceImpl.DMServiceConfiguration .class)
public class EAEMDMServiceImpl implements EAEMDMService {
private static final Logger log = LoggerFactory.getLogger(EAEMDMServiceImpl.class);

private static String APPLICATION_TEST_SERVER_CONTEXT = "application_test_server_context";

private static final String EAEM_SERVICE_USER = "eaem-service-user";

private String dmcTestContext;

@Reference
private ResourceResolverFactory resourceResolverFactory;

@Reference
private Scene7Service scene7Service;

@Reference
private S7ConfigResolver s7ConfigResolver;

@Reference
private Scene7APIClient scene7APIClient;

@Activate
@Modified
protected void activate(final DMServiceConfiguration config) {
dmcTestContext = config.dmc_test_context();

if (StringUtils.isNotEmpty(dmcTestContext)) {
dmcTestContext = dmcTestContext.trim();

if (!dmcTestContext.endsWith("/")) {
dmcTestContext = dmcTestContext + "/";
}
}

log.debug("DMC(S7) test context set in configuration - " + dmcTestContext);
}

@Override
public String getS7TestContext(final String assetPath) {
if (StringUtils.isNotEmpty(dmcTestContext)) {
log.info("DMC(S7) test context - " + dmcTestContext);
return dmcTestContext;
}

String testContext = "";

try {
final ResourceResolver s7ConfigResourceResolver = getServiceResourceResolver();

if (s7ConfigResourceResolver == null) {
return testContext;
}

S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, assetPath);

if (s7Config == null) {
s7Config = s7ConfigResolver.getDefaultS7Config(s7ConfigResourceResolver);
}

final String appSettingsTypeHandle = scene7Service.getApplicationPropertyHandle(s7Config);
final Document document = scene7APIClient.getPropertySets(appSettingsTypeHandle, s7Config);

testContext = getPropertyValue(document, APPLICATION_TEST_SERVER_CONTEXT);

if(StringUtils.isEmpty(testContext)){
testContext = "https://preview1.assetsadobe.com/";
}

if (!testContext.endsWith("/")) {
testContext = testContext + "/";
}

log.info("DMC(S7) test context read using api - " + testContext);

dmcTestContext = testContext;
} catch (final XPathExpressionException e) {
log.error("Error getting S7 test context ", e);
}

return testContext;
}

public String getS7TestContextUrl(final String assetPath, final String deliveryUrl) {
String testContextUrl = "";

if (StringUtils.isEmpty(deliveryUrl)) {
return testContextUrl;
}

String imageServerPath = "";

imageServerPath = deliveryUrl.substring(deliveryUrl.indexOf("/is/image") + 1);

testContextUrl = getS7TestContext(assetPath) + imageServerPath;

testContextUrl = testContextUrl.replace("http://", "https://");

log.debug("Rendition test context url - " + testContextUrl);

return testContextUrl;
}

private String getPropertyValue(final Document document, final String name) throws XPathExpressionException {
final XPath xpath = XPathFactory.newInstance().newXPath();
String value = "";

final String expression = getLocalName("getPropertySetsReturn") + getLocalName("setArray")
+ getLocalName("items") + getLocalName("propertyArray") + getLocalName("items");

final XPathExpression xpathExpr = xpath.compile(expression);

final NodeList nodeList = (NodeList) xpathExpr.evaluate(document, XPathConstants.NODESET);
Node nameNode, valueNode;

for (int i = 0; i < nodeList.getLength(); i++) {
nameNode = nodeList.item(i).getFirstChild();

if (!nameNode.getTextContent().equals(name)) {
continue;
}

valueNode = nodeList.item(i).getLastChild();

value = valueNode.getTextContent();

break;
}

return value;
}

private String getLocalName(final String name) {
return "/*[local-name()='" + name + "']";
}

public ResourceResolver getServiceResourceResolver() {
Map<String, Object> subServiceUser = new HashMap<>();
subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
try {
return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
} catch (Exception ex) {
log.error("Could not login as SubService user {}, exiting SearchService service.", "eaem-service-user", ex);
return null;
}
}

@ObjectClassDefinition(name = "Experience AEM Dynamic Media Configuration")
public @interface DMServiceConfiguration {

@AttributeDefinition(
name = "DMC (S7) test context",
description = "Set DMC (S7) test context (and not read it using API)",
type = AttributeType.STRING)
String dmc_test_context();
}
}


5) To get the image smart crops as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/image-smart-crops.jsp 

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

<%@page session="false"
import="java.util.Iterator,
org.apache.sling.commons.json.JSONObject,
com.adobe.granite.ui.components.Config,
com.adobe.granite.ui.components.Tag"%>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %>

<%
Config cfg = cmp.getConfig();
ValueMap dynVM = null;

JSONObject dynRenditions = new JSONObject();
Resource dynResource = null;

EAEMDMService dmcService = sling.getService(EAEMDMService.class);
response.setContentType("application/json");

String name = "Original";

JSONObject dynRendition = new JSONObject();

dynRendition.put("type", "IMAGE");
dynRendition.put("name", name);

dynRenditions.put(name, dynRendition);

DataSource rendsDS = null;

try{
rendsDS = cmp.getItemDataSource();
}catch(Exception e){
//could be pixel crop, ignore...
}

if(rendsDS == null){
dynRenditions.write(response.getWriter());
return;
}

for (Iterator<Resource> items = rendsDS.iterator(); items.hasNext();) {
dynRendition = new JSONObject();

dynResource = items.next();

dynVM = dynResource.getValueMap();

name = String.valueOf(dynVM.get("breakpoint-name"));
String testContextUrl = dmcService.getS7TestContextUrl(dynResource.getPath(), (String)dynVM.get("copyurl"));

dynRendition.put("type", "IMAGE");
dynRendition.put("name", name);
dynRendition.put("s7Url", testContextUrl);
dynRendition.put("cropdata", getCropData(dynVM));

dynRenditions.put(name, dynRendition);
}

dynRenditions.write(response.getWriter());
%>

<%!
private static JSONArray getCropData(ValueMap dynVM) throws Exception{
JSONArray cropArray = new JSONArray();
JSONObject cropData = new JSONObject();

cropData.put("name", String.valueOf(dynVM.get("breakpoint-name")));
cropData.put("id", dynVM.get("id"));
cropData.put("topN", dynVM.get("topN"));
cropData.put("bottomN", dynVM.get("bottomN"));
cropData.put("leftN", dynVM.get("leftN"));
cropData.put("rightN", dynVM.get("rightN"));

cropArray.put(cropData);

return cropArray;
}
%>


6) Set the datasource for Image Smart Crops /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource

7) To get the video encodes as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/video-encodes/video-encodes.jsp 

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

<%@page session="false"
import="java.util.Iterator,
org.apache.sling.commons.json.JSONObject,
com.adobe.granite.ui.components.Config,
com.adobe.granite.ui.components.Tag"%>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%@ page import="com.day.cq.dam.api.Asset" %>
<%@ page import="com.day.cq.dam.api.renditions.DynamicMediaRenditionProvider" %>
<%@ page import="com.day.cq.dam.api.Rendition" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.List" %>
<%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %>

<%
response.setContentType("application/json");

SlingHttpServletRequest eaemSlingRequest = slingRequest;
String assetPath = eaemSlingRequest.getRequestPathInfo().getSuffix();

Resource currentResource = eaemSlingRequest.getResourceResolver().getResource(assetPath);
Asset asset = (currentResource != null ? currentResource.adaptTo(Asset.class) : null);

EAEMDMService dmcService = sling.getService(EAEMDMService.class);
String s7Domain = dmcService.getS7TestContext(asset.getPath());

s7Domain = s7Domain.replace("http://", "https://");

JSONObject dynRenditions = new JSONObject();

if( (asset == null) || !(asset.getMimeType().startsWith("video/"))) {
dynRenditions.write(response.getWriter());
return;
}

DynamicMediaRenditionProvider dmRendProvider = sling.getService(DynamicMediaRenditionProvider.class);

HashMap<String, Object> rules = new HashMap<>();
rules.put("remote", true);
rules.put("video", true);

JSONObject dynRendition = new JSONObject();
String image = null;
String s7EncodeUrl = null;

List<Rendition> dmRenditions = dmRendProvider.getRenditions(asset, rules);

for (Rendition dmRendition : dmRenditions) {
dynRendition = new JSONObject();

image = dmRendition.getPath();

image = image.substring(0, image.lastIndexOf("."));

s7EncodeUrl = getPreviewUrl(s7Domain, dmRendition.getPath());

dynRendition.put("type", "VIDEO");
dynRendition.put("name", dmRendition.getName());
dynRendition.put("image", getRendThumbnail(s7Domain, image));
dynRendition.put("s7Url", s7EncodeUrl);

dynRenditions.put(dmRendition.getName(), dynRendition);
}

dynRenditions.write(response.getWriter());
%>

<%!
private static String getScene7Url(String s7Domain, String rendPath){
return s7Domain + "/s7viewers/html5/VideoViewer.html?asset=" + rendPath;
}

private static String getPreviewUrl(String s7Domain, String rendPath){
if(rendPath.contains(".")){
rendPath = rendPath.substring(0, rendPath.lastIndexOf("."));
}

return s7Domain + "is/content/" + rendPath;
}

private static String getRendThumbnail(String s7Domain, String rendPath){
return s7Domain + "is/image/" + rendPath + "?fit=constrain,1&wid=200&hei=200";
}
%>


8) Set the datasource for video encodes /apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource

9) Add the action bar button Smart Crop Download configuration in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-but

<?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" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/collection/action"
icon="link"
target=".cq-damadmin-admin-childpages"
text="Smart Crop Download"
variant="actionBar"/>


10) Add a client library /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib with categories=dam.gui.actions.coral and the following logic in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib/get-smart-crop-link.js to add the button to action bar, read crops and show them in a modal window...

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

var ASSET_DETAILS_PAGE = "/assetdetails.html",
initialized = false,
RENDITION_ACTIVE = ".rendition-active",
IMAGE_SMART_CROPS_URL = "/apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions.html",
VIDEO_ENCODES_URL = "/apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions.html",
BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-download-activator",
PROXY_SERLVET = "/bin/eaem/proxy?dr=",
SMART_CROP_BUTTON_URL = "/apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-but.html";

if (!isAssetDetailsPage()) {
return;
}

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

function addActionBarButtons(){
if (initialized) {
return;
}

initialized = true;

$.ajax(SMART_CROP_BUTTON_URL).done(addSmartCropUrlButton);
}

function addSmartCropUrlButton(html) {
var $eActivator = $("." + BESIDE_ACTIVATOR);

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

var $smartCropBUt = $(html).insertAfter($eActivator);

$smartCropBUt.find("coral-button-label").css("padding-left", "7px");
$smartCropBUt.click(showSmartCropUrl);
}

function showSmartCropUrl() {
var $activeRendition = $(RENDITION_ACTIVE);

if (_.isEmpty($activeRendition)) {
showAlert("Rendition not selected...", "Error");
return;
}

var title = $activeRendition.attr("title"),
assetUrl = window.location.pathname.substring(ASSET_DETAILS_PAGE.length),
assetMimeType = $(RENDITION_ACTIVE).attr("data-type"),
url = IMAGE_SMART_CROPS_URL;

if (assetMimeType && assetMimeType.toLowerCase().startsWith("video")) {
url = VIDEO_ENCODES_URL;
} else {
title = $activeRendition.find(".name").last().html();
}

return $.ajax({url: url + assetUrl}).done(function (data) {
var drUrl = data[title];

if (!drUrl) {
showAlert("Dynamic rendition url not available", "Error");
return;
}

var fui = $(window).adaptTo("foundation-ui"),
options = [{
id: "DOWNLOAD",
text: "Download"
},
{
id: "OPEN_TAB",
text: "Open"
},
{
id: "ok",
text: "Ok",
primary: true
}];

fui.prompt("Rendition Url", drUrl["s7Url"], "default", options, function (actionId) {
if (actionId === "OPEN_TAB") {
window.open(drUrl["s7Url"], '_blank');
}else if (actionId === "DOWNLOAD") {
var downloadUrl = PROXY_SERLVET + drUrl["s7Url"];
window.open(downloadUrl, '_blank');
}
});
});
}

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

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

fui.prompt(title, message, type, options, callback);
}

function isAssetDetailsPage() {
return (window.location.pathname.indexOf(ASSET_DETAILS_PAGE) >= 0);
}
}(jQuery, jQuery(document)));


AEM Cloud Service - Sling Post Processor to remove duplicate values with Bulk Editor Append Mode

$
0
0

Goal

AEM Cloud Version :  2021.3.5087.20210322T071003Z-210225 (March 22, 2021)

Using Append mode in Bulk Edit, duplicate tag values could be inserted in custom metadata field when Tag widget is used in schema. Sling Post Processor below scrubs the data and removes any duplicates when assets metadata is updated using bulk editor...

Package Install | Github


Product


Extension


Solution

1) Add a bulk update sling post processor apps.experienceaem.assets.core.listeners.BulkUpdateAppendPostProcessor to remove any duplicate values...

package apps.experienceaem.assets.core.listeners;

import org.apache.commons.lang.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.ModificationType;
import org.apache.sling.servlets.post.SlingPostProcessor;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Component(
immediate = true,
service = { SlingPostProcessor.class },
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
}
)
public class BulkUpdateAppendPostProcessor implements SlingPostProcessor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private static final String MODE = "mode";
private static final String MODE_SOFT = "soft";
private static final String BULK_UPDATE = "dam:bulkUpdate";

public void process(SlingHttpServletRequest request, List<Modification> modifications) throws Exception {
try{
ResourceResolver resolver = request.getResourceResolver();
String reqType = request.getParameter(BULK_UPDATE);
String reqMode = request.getParameter(MODE);

if ( (reqType == null) || (reqMode == null) || !reqMode.equals(MODE_SOFT)) {
return;
}

Session session = resolver.adaptTo(Session.class);
RequestParameter[] assets = request.getRequestParameters("asset");

if ( (session == null) || (assets == null)) {
return;
}

for (Modification change : modifications) {
if (!change.getType().equals(ModificationType.MODIFY)) {
continue;
}

processChanges(session, change);
}

session.save();
} catch (Exception e) {
logger.error("Error updating multi valued properties during bulk edit", e);
}
}

private void processChanges(Session session, Modification change) throws Exception{
Item jcrItem = null;

String changedMetadataPath = change.getSource();

if( changedMetadataPath == null ){
return;
}

jcrItem = session.getItem(changedMetadataPath);

if ( (jcrItem == null) || jcrItem.isNode()) {
return;
}

Node metadataNode = null;
Property metaProp = null;

String changedMetadataNodePath = changedMetadataPath.substring(0, changedMetadataPath.lastIndexOf("/"));
String changedPropName = changedMetadataPath.substring(changedMetadataPath.lastIndexOf("/") + 1);

if(!session.itemExists(changedMetadataNodePath)){
return;
}

metadataNode = session.getNode(changedMetadataNodePath);
metaProp = metadataNode.getProperty(changedPropName);

if(!metaProp.isMultiple()){
return;
}

Value values[] = getNewTagValues(metaProp.getValues());

if(ArrayUtils.isEmpty(values)){
metaProp.remove();
}else{
metadataNode.setProperty(changedPropName, values);
}
}

private Value[] getNewTagValues(Value[] oldValues) throws Exception{
List<Value> newValues = new ArrayList<Value>();
List<String> existingTags = new ArrayList<String>();

for(Value value : oldValues){
if(existingTags.contains(value.getString())){
continue;
}

newValues.add(value);
existingTags.add(value.getString());
}

return newValues.toArray(new Value[newValues.size()]);
}
}




AEM Cloud Service - Assets Folder Properties Dynamic Media Image Profiles Sorted Case Ignored

$
0
0

Goal

Sample code in this post is for Case Ignored Sorting of Dynamic Media Image Profiles in folder properties

Package Install | Github


Product


Extension


Solution

Create a cq:ClientLibrayFolder /apps/eaem-cs-sort-image-profiles/clientlibs/sort-image-profiles with categories=cq.gui.damadmin.v2.foldershare.coral and a JS file /apps/eaem-cs-sort-image-profiles/clientlibs/sort-image-profiles/sort-profiles.js with the following logic, to read the select ./jcr:content/imageProfile and order the values....

(function ($, $document) {
"use strict";
var IMAGE_PROFILE_SEL = "[name='./jcr:content/imageProfile']";

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

function sortImageProfiles(){
var $imageProfileSelect = $(IMAGE_PROFILE_SEL);

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

var selectItems = $imageProfileSelect[0].items,
values = selectItems.getAll(),
noneValue = values[0];

values.sort(function (a, b) {
var nameA = a.textContent.toUpperCase();
var nameB = b.textContent.toUpperCase();

return ((nameA < nameB) ? -1 : ((nameA > nameB) ? 1 : 0));
});

selectItems.clear();

selectItems.add(noneValue);

_.each(values, function(value){
if(value.textContent === "None"){
return;
}

selectItems.add(value);
});
}

}(jQuery, jQuery(document)));




AEM Cloud Service - Create Asset Servlet for Uploading Small files using CURL or from Third Party Apps

$
0
0

Goal

Uploading assets to AEM Cloud Service is a 3 step process (initiateUpload.json, Upload to blob storage, completeUpload.json). Tools like https://github.com/adobe/aem-upload can be used to upload files to AEM using a NodeJS client...

This post is on creating simple assets (text , json files etc.) in AEM using CURL or other third party apps in a single step (in traditional AEMs this is provided by .createasset.htmlcheck documentation). Pass the fileName and fileContent parameters to create assets in AEM with this servlet installed...

Demo | Package Install | Github


Get Access Token from Developer Console


CURL Request using Bearer

            curl -X POST -d "fileName=2_eaem.txt&fileContent=TWO_EXPERIENCE_AEM" -H "Authorization: Bearer eyJhbGINFINITY9SLA" https://author-pxxxxx-exxxxx.adobeaemcloud.com/content/dam/experience-lakes.eaemcreateasset.html



Asset Created in AEM


Solution

Add a servlet apps.experienceaem.assets.core.servlets.EAEMCreateAssetServlet to create the assets in AEM...

package apps.experienceaem.assets.core.servlets;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;

@Component(
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.selectors=eaemcreateasset",
"sling.servlet.methods=POST",
"sling.servlet.resourceTypes=sling/servlet/default"
}
)
public class EAEMCreateAssetServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(EAEMCreateAssetServlet.class);

private static String INITIATE_UPLOAD_REQ = ".initiateUpload.json";
public static String FILE_NAME = "fileName";
public static String UPLOAD_TOKEN = "uploadToken";
public static String MIME_TYPE = "mimeType";
public static String FILE_SIZE = "fileSize";

@Reference
private HttpClientBuilderFactory httpClientBuilderFactory;

private CloseableHttpClient httpClient;

protected void activate(ComponentContext ctx) {
HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();

RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000)
.setSocketTimeout(30000).build();

builder.setDefaultRequestConfig(requestConfig);

httpClient = builder.build();
}

protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
String fileName = request.getParameter("fileName");
String fileContent = request.getParameter("fileContent");

try {
if (StringUtils.isEmpty(fileName)) {
response.sendError(SlingHttpServletResponse.SC_FORBIDDEN, "fileName parameter missing");
return;
}

int fileSize = fileContent.length();
request.setAttribute(FILE_NAME, fileName);
request.setAttribute(FILE_SIZE, fileSize);

String initiateUploadResponse = makeInitiateUploadRequest(request, response);

log.debug("initiateUpload Response : " + initiateUploadResponse);

JSONObject uploadResponse = new JSONObject(initiateUploadResponse);
JSONArray filesJSON = uploadResponse.getJSONArray("files");
JSONObject fileJSON = (JSONObject) filesJSON.get(0);

String binaryPOSTUrl = fileJSON.getJSONArray("uploadURIs").getString(0);

HttpPut put = new HttpPut(binaryPOSTUrl);
HttpEntity entity = new StringEntity(fileContent);

put.setEntity(entity);

HttpResponse putResponse = httpClient.execute(put);
int statusCode = putResponse.getStatusLine().getStatusCode();

if( (statusCode < 200) || (statusCode > 210)){
response.sendError(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error uploading file - " + putResponse.getStatusLine().getReasonPhrase());
return;
}

String completedResponse = makeCompleteUploadRequest(uploadResponse, request, response);

log.debug("completeUpload Response : " + completedResponse);

response.getWriter().print(fileName);
} catch (Exception e) {
log.error("Error creating file : {}", fileName);
response.sendError(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error creating file - " + e.getMessage());
}
}

private String makeCompleteUploadRequest(JSONObject uploadResponse, SlingHttpServletRequest request,
SlingHttpServletResponse response) throws Exception{
JSONArray filesJSON = uploadResponse.getJSONArray("files");
JSONObject fileJSON = (JSONObject) filesJSON.get(0);

String uploadToken = fileJSON.getString("uploadToken");
String mimeType = fileJSON.getString("mimeType");

String completeURI = uploadResponse.getString("completeURI");

request.setAttribute(UPLOAD_TOKEN, uploadToken);
request.setAttribute(MIME_TYPE, mimeType);

SlingHttpServletRequest wrapperRequest = new FileNameSlingServletRequestWrapper(request);
SlingHttpServletResponse wrapperResponse = new EAEMSlingModelResponseWrapper(response);

RequestDispatcher dp = wrapperRequest.getRequestDispatcher(completeURI);

dp.include(wrapperRequest, wrapperResponse);

return wrapperResponse.toString();
}

private String makeInitiateUploadRequest(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws Exception{
String folderPath = request.getResource().getPath();

SlingHttpServletRequest wrapperRequest = new FileNameSlingServletRequestWrapper(request);
RequestDispatcher dp = wrapperRequest.getRequestDispatcher(folderPath + INITIATE_UPLOAD_REQ);

SlingHttpServletResponse wrapperResponse = new EAEMSlingModelResponseWrapper(response);

dp.include(wrapperRequest, wrapperResponse);

String uploadResponseStr = wrapperResponse.toString();

if(StringUtils.isEmpty(uploadResponseStr)){
response.sendError(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Empty upload response, file creation failed");
return "";
}

//uploadResponseStr = getTestInitiateUploadResponse();

return uploadResponseStr;
}


private class EAEMSlingModelResponseWrapper extends SlingHttpServletResponseWrapper {
private CharArrayWriter writer;

public EAEMSlingModelResponseWrapper (final SlingHttpServletResponse response) {
super(response);
writer = new CharArrayWriter();
}

public PrintWriter getWriter() throws IOException {
return new PrintWriter(writer);
}

public String toString() {
return writer.toString();
}
}

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

public String[] getParameterValues(String paramName) {
if(EAEMCreateAssetServlet.FILE_NAME.equals(paramName)){
return new String[] { String.valueOf(super.getAttribute(FILE_NAME)) };
}else if(EAEMCreateAssetServlet.FILE_SIZE.equals(paramName)){
return new String[] { String.valueOf(super.getAttribute(FILE_SIZE)) };
}else if(EAEMCreateAssetServlet.UPLOAD_TOKEN.equals(paramName)){
return new String[] { (String)super.getAttribute(UPLOAD_TOKEN) };
}else if(EAEMCreateAssetServlet.MIME_TYPE.equals(paramName)){
return new String[] { (String)super.getAttribute(MIME_TYPE) };
}

return super.getParameterValues(paramName);
}
}
}


To make a CURL request using login-token

curl -X POST -d "fileName=1_eaem.txt&fileContent=ONE_EXPERIENCE_AEM" -b "login-token=login%3aexxx4g%3acrx.default; Path=/; Domain=.author-pxxx-exx.adobeaemcloud.com; Expires=Sat, 16 Apr 2022 20:20:07 GMT;" https://author-pxxx-exxx.adobeaemcloud.com/content/dam/experience-lakes.eaemcreateasset.html


AEM Cloud Service - Show Image Previews in Path Field and Multifield of Content Fragments

$
0
0

Goal

Show Image Previews in Content Fragment Editor, when selected using a Path Field or Path Field in Multifield...

Demo | Package Install | Github


Product


Extension


Solution

Add a client library /apps/eaem-cs-cf-image-preview/clientlibs/image-preview with categories=dam.cfm.adminpage.v2 and the following logic in /apps/eaem-cs-cf-image-preview/clientlibs/image-preview/image-preview.js to add preview cards...

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

var _ = window._,
initialized = false,
EDITOR_PAGE = "/editor.html",
FRAGMENT_EDITOR_PAGE = "/mnt/overlay/dam/cfm/admin/content/v2/fragment-editor.html",
IMAGE_REF_SELECTOR = "foundation-autocomplete",
MF_SELECTOR = "coral-multifield",
IMAGE_PREVIEW_CARD = "eaem-image-preview-card";

if (!isFragmentEditorPage()) {
return;
}

init();

function init(){
if(initialized){
return;
}

initialized = true;

window.Dam.CFM.Core.registerReadyHandler(extendFragmentEditor);
}

function extendFragmentEditor(){
addImagePreviews();

addImagePreviewsInMF();
}

function addImagePreviewsInMF(){
$(MF_SELECTOR).each(function(index, mField){
Coral.commons.ready(mField, addImageCard);
});

$(MF_SELECTOR).on("coral-collection:add", function(event){
Coral.commons.ready(event.detail.item, handleMFAdd);
});

function handleMFAdd(mfItem){
$(mfItem).find("foundation-autocomplete").on("change", showImageInMF);
}

function addImageCard(mField){
var $multiField = $(mField);

_.each($multiField[0].items.getAll(), function(mfItem) {
var $content = $(mfItem.content),
$imgReference = $content.find("foundation-autocomplete");

$imgReference.on("change", showImageInMF);

showImageInMF.call($imgReference[0]);
});
}

function showImageInMF(){
var $imageField = $(this),
$fieldWrapper = $imageField.closest("coral-multifield-item"),
imageUrl = this.value,
fileName = imageUrl.substring(imageUrl.lastIndexOf("/") + 1);

imageUrl = imageUrl + "/_jcr_content/renditions/cq5dam.thumbnail.319.319.png";

if($fieldWrapper.find("." + IMAGE_PREVIEW_CARD).length > 0){
$fieldWrapper.find("." + IMAGE_PREVIEW_CARD).remove();
}

$.ajax(imageUrl).done(function(){
$(getCardContent(imageUrl, fileName)).appendTo($fieldWrapper);
});
}
}

function getCardContent(imageUrl, fileName){
return '<div class="' + IMAGE_PREVIEW_CARD + '">' +
'<coral-card fixedwidth assetwidth="200" assetheight="200">' +
'<coral-card-asset><img src="' + imageUrl + '"></coral-card-asset>' +
'<coral-card-content>' +
'<coral-card-title>' + fileName + '</coral-card-title>' +
'</coral-card-content>' +
'</coral-card>' +
'</div>';
}

function addImagePreviews(){
$(IMAGE_REF_SELECTOR).each(function(index, imageField){
Coral.commons.ready(imageField, function(){
showImage.call(imageField);
$(imageField).on("change", showImage);
});
});

function showImage(){
var $imageField = $(this),
$fieldWrapper = $imageField.parent(),
imageUrl = this.value,
fileName = imageUrl.substring(imageUrl.lastIndexOf("/") + 1);

if(!$fieldWrapper.hasClass("coral-Form-fieldwrapper")){
return;
}

if($fieldWrapper.find("." + IMAGE_PREVIEW_CARD).length > 0){
$fieldWrapper.find("." + IMAGE_PREVIEW_CARD).remove();
}

imageUrl = imageUrl + "/_jcr_content/renditions/cq5dam.thumbnail.319.319.png";

$.ajax(imageUrl).done(function(){
$(getCardContent(imageUrl, fileName)).appendTo($fieldWrapper);
});
}
}

function isFragmentEditorPage() {
return (window.location.pathname.indexOf(FRAGMENT_EDITOR_PAGE) >= 0)
|| (window.location.pathname.indexOf(EDITOR_PAGE) >= 0);
}
}(jQuery, jQuery(document)));


AEM Cloud Service - Configure file upload max size per asset type, restrict upload from UI

$
0
0

Goal

AEM Cloud Version : 2021.4.5226.20210427T070726Z-210325 (April 27, 2021)

Maximum asset size for uploads from AEM UI can be configured by overlaying /libs/dam/gui/content/assets/jcr:content/actions/secondary/create/items/fileupload@sizeLimit in /apps, however if the requirement is to configure limit per asset type (eg. jpg, png, mp4 etc.) the following extension could be useful....

Demo | Package Install | Github


Error shown in UI


Custom Configuration Nav



Configure Limit

                                 https://author-pxxxx-exxxx.adobeaemcloud.com/apps/eaem-cs-restrict-assets-size/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config


Limit Saved in CRX


Solution

1) Create a custom widget /apps/eaem-cs-restrict-assets-size/dam-tools/textfield/textfield.jsp to read the values from config resource /conf/global/settings/dam/eaem-dam-config

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

<%@page session="false"
import="com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Config,
com.adobe.granite.ui.components.Tag" %>
<%
Config cfg = cmp.getConfig();

String CONFIG_RES = "/conf/global/settings/dam/eaem-dam-config";
Resource configRes = resourceResolver.getResource(CONFIG_RES);

String name = cfg.get("name", String.class);
String value = "";

if(configRes != null){
value = configRes.getValueMap().get(name, String.class);
}

if(value == null){
value = cfg.get("defaultValue", String.class);
}

if(value == null){
value = "";
}

Tag tag = cmp.consumeTag();

AttrBuilder attrs = tag.getAttrs();
cmp.populateCommonAttrs(attrs);

attrs.add("name", name);

String fieldLabel = cfg.get("fieldLabel", String.class);
String fieldDesc = cfg.get("fieldDescription", String.class);
%>

<div class="coral-Form-fieldwrapper">
<label class="coral-Form-fieldlabel"><%=fieldLabel%></label>
<input is="coral-textfield" name="<%=name%>" value="<%=value%>" style="width: 100%;">
<coral-icon class="coral-Form-fieldinfo" icon="infoCircle" size="S"></coral-icon>
<coral-tooltip target="_prev" placement="left" class="coral3-Tooltip" variant="info" role="tooltip" style="display: none;">
<coral-tooltip-content><%=fieldDesc%></coral-tooltip-content>
</coral-tooltip>
</div>


2) Create the tools navigation /apps/cq/core/content/nav/tools/eaem-dam-tools/eaem-dam-config

<?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:description="EAEM Custom Configuration"
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Configuration"
href="/apps/eaem-cs-restrict-assets-size/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config"
icon="asset"
id="eaem-dam-config"
size="XL"/>


3) Create the configuration page /apps/eaem-cs-restrict-assets-size/dam-tools/eaem-dam-config saving max upload limits to /conf/global/settings/dam/eaem-dam-config accessed using Tools> EAEM Configuration> EAEM Configuration or https://author-pxxxxx-exxxxx.adobeaemcloud.com/apps/eaem-cs-restrict-assets-size/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config

<?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="EAEM DAM Configuration"
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-dam-config"
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-cs-restrict-assets-size/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config"
text="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="Configuration"
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">
<png-size
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-cs-restrict-assets-size/dam-tools/textfield"
fieldDescription="Enter the max size allowed for PNG upload, in bytes..."
fieldLabel="PNG upload max size allowed"
name="./pngSize"/>
<jpg-size
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-cs-restrict-assets-size/dam-tools/textfield"
fieldDescription="Enter the max size allowed for JPG upload, in bytes..."
fieldLabel="JPG upload max size allowed"
name="./jpgSize"/>
<mp4-size
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-cs-restrict-assets-size/dam-tools/textfield"
fieldDescription="Enter the max size allowed for MP4 upload, in bytes..."
fieldLabel="MP4 upload max size allowed"
name="./mp4Size"/>
</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) Create a clientlib /apps/eaem-cs-restrict-assets-size/supported-file-sizes with categories=dam.gui.coral.fileupload and dependencies=eaem.lodash

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

var _ = window._,
CONFIG_PATH = "/conf/global/settings/dam/eaem-dam-config.json",
allowedSizes = {},
ENDS_WITH_SIZE = "Size";

loadAllowedSizes();

var _origConfirmUpload = window.DamFileUpload.prototype._confirmUpload,
_origOnInputChange = window.Dam.ChunkFileUpload.prototype._onInputChange;

window.Dam.ChunkFileUpload.prototype._onInputChange = function(event){
var files = event.target.files;

if(!files && event.dataTransfer && event.dataTransfer.files){
files = event.dataTransfer.files;
}

if(_.isEmpty(files)){
_origOnInputChange.call(this, event);
return;
}

var errorMessage = "";

_.each(files, function(file){
var fileErrorMessage = checkWithinSize(file);

if(!fileErrorMessage){
return;
}

errorMessage = errorMessage + fileErrorMessage;
});

if(errorMessage){
showAlert(errorMessage);
}else{
_origOnInputChange.call(this, event);
}
};

window.DamFileUpload.prototype._confirmUpload = function (event) {
var errorMessage = "";

this.fileUpload.uploadQueue.forEach(function(item) {
var fileErrorMessage = checkWithinSize(item);

if(!fileErrorMessage){
return;
}

errorMessage = errorMessage + fileErrorMessage;
});

if(errorMessage){
showAlert(errorMessage);
}else{
_origConfirmUpload.call(this, event);
}
};

function checkWithinSize(file){
var fileName = file.name, errorMessage = "";

if(!fileName.includes(".")){
return;
}

var ext = fileName.substring(fileName.lastIndexOf(".") + 1);

_.each(allowedSizes, function(allowedSize, fileType){
if(fileType !== ext){
return;
}

if(file.size > allowedSize){
errorMessage = "<b>" + fileName + "</b> size <b>" + formatBytes(file.size, 2)
+ "</b> is more than allowed <b>" + formatBytes(allowedSize, 2) + "</b>";
}
});

return errorMessage;
}

function loadAllowedSizes(){
$.ajax(CONFIG_PATH).done(function(data){
if(_.isEmpty(data)){
return;
}

_.each(data, function(value, key){
if(!key.endsWith(ENDS_WITH_SIZE)){
return;
}

allowedSizes[key.substring(0, key.lastIndexOf(ENDS_WITH_SIZE))] = parseInt(value);
})
})
}

function formatBytes(bytes, decimals) {
if (bytes === 0){
return '0 Bytes';
}

const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));

return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + '' + sizes[i];
}

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, "warning", options, callback);
}
}(jQuery, jQuery(document)));



AEM Cloud Service - Render condition to show Reprocess Assets button for configured group

$
0
0

Goal


AEM Cloud Version : 2021.4.5226.20210427T070726Z-210325 (April 27, 2021)

A sample Render Condition to allow specific users (part of a group) reprocess assets (by clicking the Reprocess Assets in action bar). The group configuration is provided in Tools> Experience AEM> EAEM Configuration



Tools Navigation



Group Configuration



Saved in CRX

                                 /conf/global/settings/dam/eaem-dam-config




Reprocess Assets




Solution


1) Create a custom widget /apps/eaem-cs-smart-crop-rc/dam-tools/userpicker/userpicker.jsp to read the values from config resource /conf/global/settings/dam/eaem-dam-config. It extends otb user picker /libs/granite/ui/components/coral/foundation/form/userpicker using a sling:include, for setting the granite.ui.form.contentpath attribute to read values from config location  /conf/global/settings/dam/eaem-dam-config
<%@include file="/libs/granite/ui/global.jsp" %>

<%@page session="false"%>

<%
String CONFIG_RES = "/conf/global/settings/dam/eaem-dam-config";

request.setAttribute("granite.ui.form.contentpath", CONFIG_RES);
%>

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

2) Create the tools navigation /apps/cq/core/content/nav/tools/eaem-dam-tools/eaem-dam-config.

<?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:description="EAEM Custom Configuration"
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Configuration"
href="/apps/eaem-cs-smart-crop-rc/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config"
icon="asset"
id="eaem-dam-config"
size="XL"/>

3) Create the configuration page /apps/eaem-cs-smart-crop-rc/dam-tools/eaem-dam-config for selecting the group name and save to /conf/global/settings/dam/eaem-dam-config@processAssetsGroup accessed using Tools > Experience AEM EAEM Configuration or https://author-pxxxxx-exxxxx.adobeaemcloud.com/apps/eaem-cs-restrict-assets-size/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config

<?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="EAEM DAM Configuration"
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-dam-config"
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-cs-smart-crop-rc/dam-tools/eaem-dam-config.html/conf/global/settings/dam/eaem-dam-config"
text="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="Configuration"
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">
<process-assets-group
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-cs-smart-crop-rc/dam-tools/userpicker"
fieldDescription="Select the group with process assets access"
fieldLabel="Group name"
groupsOnly="{Boolean}true"
hideServiceUsers="{Boolean}true"
multiple="{Boolean}false"
name="./processAssetsGroup"/>
</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 render condition script /apps/eaem-cs-smart-crop-rc/smart-crops-render-condition/smart-crops-render-condition.jsp checking if the user is member of group set in /conf/global/settings/dam/eaem-dam-config@processAssetsGroup.

<%@page session="false"
import="org.apache.sling.api.resource.Resource,
com.adobe.granite.ui.components.rendercondition.RenderCondition,
com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition" %>
<%@ page import="org.apache.jackrabbit.api.security.user.UserManager" %>
<%@ page import="org.apache.jackrabbit.api.security.user.Authorizable" %>
<%@ page import="org.apache.jackrabbit.api.security.user.Group" %>
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.2" %>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0" %>

<sling:defineObjects/>
<cq:defineObjects/>

<%
boolean allowed = false;

try{
String CONFIG_RES = "/conf/global/settings/dam/eaem-dam-config";
Resource configRes = resourceResolver.getResource(CONFIG_RES);
String assetProcessGroup = configRes.getValueMap().get("processAssetsGroup", String.class);

if(StringUtils.isNotEmpty(assetProcessGroup)){
UserManager userManager = resourceResolver.adaptTo(UserManager.class);

Authorizable user = userManager.getAuthorizable(slingRequest.getUserPrincipal().getName());
Group group = (Group)userManager.getAuthorizable(assetProcessGroup);

allowed = ((group != null) && group.isMember(user));
}
}catch(Exception e){
allowed = false;
}

request.setAttribute(RenderCondition.class.getName(), new SimpleRenderCondition(allowed));
%>

5) Create a overlay configuration of Reprocess Assets button in Assets console, Asset Details, Omni Search, Collections and set the render condition...

             /apps/dam/gui/content/assets/jcr:content/actions/selection/reprocessassets/granite:rendercondition/process-assets

     /apps/dam/gui/content/assetdetails/jcr:content/actions/reprocessassets/granite:rendercondition/process-assets

     /apps/dam/gui/content/collections/collectiondetails/jcr:content/actions/selection/reprocessassets/granite:rendercondition/process-assets

     /apps/granite/omnisearch/content/metadata/asset/actions/selection/reprocessassets/granite:rendercondition/process-assets






AEM Cloud Service - Extend React SPA Sling Model for Page Style System support

$
0
0

Goal

Extend the JSON model eg. /content/eaem-cs-spa-style-system/us/en.model.json for supporting Page Style System in a React SPA. The following extension adds page level css classes configured via Style System in the model's children cssClassNames property eg.spa page basicpage eaempage--background-aero and necessary logic on the react side to apply it...  

Demo | Package Install | Content Package | Github


Style System in Template Policy


Styles on Page


Style CSS in Model



Solution

1) Create the project using following maven archetype command

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=24 -D aemVersion=cloud -D appTitle="Experience AEM SPA Style System" -D appId="eaem-cs-spa-style-system"
-D groupId="apps.experienceaem.sites.spa" -D frontendModule=react -D includeExamples=n -D includeDispatcherConfig=n


2) Create the CSS file for Page Style System classes /apps/eaem-cs-spa-style-system/clientlibs/clientlib-base/main.css with the following code, add it in /apps/eaem-cs-spa-style-system/clientlibs/clientlib-base/css.txt

.eaempage--background-gray{
background-color: #f1f1f1;
}

.eaempage--background-white{
background-color: #ffffff;
}

.eaempage--background-black{
background-color: #000000;
}

.eaempage--background-beige{
background-color: #EEE1C6;
}

.eaempage--background-aero{
background-color: #CAF1DE;
}


3) Add a filter apps.experienceaem.sites.spa.core.filters.EAEMDefaultModelJSONFilter for intercepting model.json requests, iterate child pages and add the configured style classes in property cssClassNames 

package apps.experienceaem.sites.spa.core.filters;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import com.day.cq.wcm.api.policies.ContentPolicy;
import com.day.cq.wcm.api.policies.ContentPolicyManager;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONArray;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.framework.Constants;

import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.*;
import java.io.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Default Sling Model Response Modifier Servlet Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=.*.model.json"
}
)
public class EAEMDefaultModelJSONFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(EAEMDefaultModelJSONFilter.class);

public static String EAEM_DATA = "eaemData";
private static final String SLING_VANITYPATH = "sling:vanityPath";
private static final String CQ_STYLE_IDS = "cq:styleIds";
private static final String SLING_VANITYPATH_JSON_PROP = "slingVanityPath";
private static final String CSS_CLASS_NAMES = "cssClassNames";
private static final String CHILDREN = ":children";

@Reference
private QueryBuilder builder;

@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 uri = slingRequest.getRequestURI();

if(!uri.endsWith(".model.json")){
chain.doFilter(request, response);
return;
}

SlingHttpServletResponse modelResponse = new DefaultSlingModelResponseWrapper((SlingHttpServletResponse)response);

chain.doFilter(slingRequest, modelResponse);

PrintWriter responseWriter = response.getWriter();

responseWriter.write(getModifiedContent(modelResponse.toString(), slingRequest));
}

private String getModifiedContent(String origContent, SlingHttpServletRequest slingRequest){
String modifiedContent = origContent;

try{
JSONObject model = new JSONObject(origContent);

addAddnPropertiesInPageModel(model, slingRequest);

model = (JSONObject) replaceEaemDataObject(model);

modifiedContent = model.toString();
}catch(Exception e){
log.error("Error modifying model JSON content", e);
modifiedContent = origContent;
}

return modifiedContent;
}

private void addAddnPropertiesInPageModel(JSONObject model, SlingHttpServletRequest slingRequest) throws Exception{
if(!model.has(CHILDREN)){
return;
}

JSONObject childrenModel = model.getJSONObject(CHILDREN);
Iterator<String> childrenItr = childrenModel.keys();
ResourceResolver resolver = slingRequest.getResourceResolver();
Resource pageContent;

while(childrenItr.hasNext()) {
String pagePath = childrenItr.next();
JSONObject childData = childrenModel.getJSONObject(pagePath);

pageContent = resolver.getResource(pagePath + "/jcr:content");

if(pageContent == null){
continue;
}

ValueMap vm = pageContent.getValueMap();

String[] slingVanityPaths = vm.get(SLING_VANITYPATH, String[].class);

if(ArrayUtils.isNotEmpty(slingVanityPaths)){
JSONArray vanityPaths = new JSONArray();

Arrays.stream(slingVanityPaths).forEach(vanityPaths::put);

childData.put(SLING_VANITYPATH_JSON_PROP, vanityPaths);
}

if(!childData.has(CSS_CLASS_NAMES)){
continue;
}

String styles = childData.getString(CSS_CLASS_NAMES);
String addnClasses = getCssClasses(resolver, pagePath, vm.get(CQ_STYLE_IDS, String[].class));

if(!styles.contains(addnClasses)){
childData.put(CSS_CLASS_NAMES, styles + "" + addnClasses);
}
}
}

private String getCssClasses(ResourceResolver resolver, String pagePath, String[] styleIds) throws Exception{
ContentPolicyManager policyManager = resolver.adaptTo(ContentPolicyManager.class);

Resource contentPolicyResource = getContentPolicyResource(resolver, resolver.getResource(pagePath));

if( (contentPolicyResource == null) || ArrayUtils.isEmpty(styleIds)){
return "";
}

String styleClasses = "";

for(String styleId : styleIds){
Query query = builder.createQuery(PredicateGroup.create(getStyleQueryPredicateMap(contentPolicyResource.getPath(), styleId)),
resolver.adaptTo(Session.class));

SearchResult result = query.getResult();

for (Hit hit : result.getHits()) {
styleClasses = styleClasses + hit.getProperties().get("cq:styleClasses") + "";
}

}

return styleClasses.trim();
}

private Resource getContentPolicyResource(ResourceResolver resolver, Resource pageRes) {
ContentPolicyManager policyManager = resolver.adaptTo(ContentPolicyManager.class);

if (policyManager == null) {
return null;
}

ContentPolicy policy = policyManager.getPolicy(pageRes);

if (policy == null) {
return null;
}

return policy.adaptTo(Resource.class);
}

private static Map<String, String> getStyleQueryPredicateMap(String stylePath, String styleId) {
Map<String, String> map = new HashMap<>();
map.put("path", stylePath);
map.put("1_property","cq:styleId");
map.put("1_property.value",styleId);
map.put("p.hits","selective");
map.put("p.properties","cq:styleClasses");

return map;
}

private Object replaceEaemDataObject(JSONObject jsonObject) throws Exception{
Iterator<String> itr = jsonObject.keys();
String key;

JSONObject modJSONObj = new JSONObject();
Object jsonValue = null;

while(itr.hasNext()){
key = itr.next();

if(key.equals(EAEM_DATA)){
JSONObject eaemData = (JSONObject)jsonObject.get(EAEM_DATA);

eaemData.put(":type" , jsonObject.get(":type"));

return eaemData;
}else{
jsonValue = jsonObject.get(key);

if(JSONObject.class.isInstance(jsonValue)){
modJSONObj.put(key, replaceEaemDataObject((JSONObject)jsonValue));
}else{
modJSONObj.put(key, jsonValue);
}
}
}

return modJSONObj;
}

@Override
public void destroy() {
}

private class DefaultSlingModelResponseWrapper extends SlingHttpServletResponseWrapper {
private CharArrayWriter writer;

public DefaultSlingModelResponseWrapper (final SlingHttpServletResponse response) {
super(response);
writer = new CharArrayWriter();
}

public PrintWriter getWriter() throws IOException {
return new PrintWriter(writer);
}

public String toString() {
return writer.toString();
}
}
}


4) Apply the cssClassNames (line #8) in eaem-cs-spa-style-system\ui.frontend\src\App.js

import { Page, withModel } from '@adobe/cq-react-editable-components';
import React from 'react';

// This component is the application entry point
class App extends Page {
render() {
return (
<div className={this.props.cssClassNames}>
{this.childComponents}
{this.childPages}
</div>
);
}
}

export default withModel(App);


AEM Cloud Service - Digital OnBoarding Process, Post Lead Form Data to a AEM React SPA

$
0
0

Goal

Process detailed below explains a sample Digital OnBoarding Flow, where a potential customer enters basic information in a Lead Entry page setup on AEM Sites. Form data can be stored in AEM or Adobe Campaign or any external system (for email outreach incase user does not continue with the process) and the user is later POST-forwarded to a Digital OnBoarding AEM React Spa App which shows the basic information entered in lead entry page and more steps for converting the lead to a customer. This post does not discuss Analytics and Campaign pieces...

Demo | Package Install | Content Package | Github


The Flow



Lead Entry Form



Digital On-Boarding SPA Authoring


Digital On-Boarding SPA

Solution

1) Create the AEM React SPA project using archetype https://github.com/adobe/aem-project-archetype

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=24 -D aemVersion=cloud -D appTitle="Experience AEM SPA read Post data"
-D appId="eaem-cs-spa-read-post-data" -D groupId="apps.experienceaem.sites.spa"
-D frontendModule=react -D includeExamples=n -D includeDispatcherConfig=n


2) Create the Lead Entry Form Component /apps/eaem-cs-spa-read-post-data/components/steps extending the core/wcm/components/title/v2/title component. Add additional fields in dialog for enabling them in end user form...

<?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">
<content jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<tabs jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<eaem
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
jcr:title="Enable the fields for this step"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<name
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./showName"
text="Show Name"
value="true"/>
<email
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./showEmail"
text="Show Email"
value="true"/>
<ssn
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./showSSN"
text="Show SSN"
value="true"/>
<company
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./showCompany"
text="Show Company"
value="true"/>
<previousLink
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldDescription="Select the Previous step"
fieldLabel="Previous step"
name="./previousLink"
rootPath="/content/eaem-cs-spa-read-post-data/us/en"/>
<nextLink
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldDescription="Select the Next step"
fieldLabel="Next step"
name="./nextLink"
rootPath="/content/eaem-cs-spa-read-post-data/us/en"/>
</items>
</column>
</items>
</eaem>
</items>
</tabs>
</items>
</content>
</jcr:root>


3) Add the HTL script for rendering the lead entry form component /apps/eaem-cs-spa-read-post-data/components/steps/steps.html

&ltform method="post" action="/content/eaem-cs-spa-read-post-data/us/en/home.html"&gt
&ltdiv&gt
&lth1 style="text-align: center;"&gtLead Form&lt/h1&gt

&ltdiv class='eaem-info'&gt
&ltspan&gtEnter name&lt/span&gt
&ltinput name='eaemName'/&gt
&lt/div&gt

&ltdiv class='eaem-info'&gt
&ltspan&gtEnter email&lt/span&gt
&ltinput name='eaemEmail'/&gt
&lt/div&gt
&lt/div&gt

&ltdiv style="text-align: center; margin-top: 40px"&gt&ltinput type="submit"/&gt&lt/div&gt
&lt/form&gt


4) Since the data in Lead entry form is POSTed to SPA app (for security, so the user info is not visible in URL as with GET) hosted on url /content/eaem-cs-spa-read-post-data/us/en/home.html, add the path in CSRF filter configuration eaem-cs-spa-read-post-data\ui.config\src\main\content\jcr_root\apps\eaem-cs-spa-read-post-data\osgiconfig\config\com.adobe.granite.csrf.impl.CSRFFilter.config


5) Add a filter apps.experienceaem.sites.spa.core.filters.OnBoardingPostModifierFilter to convert the POST request to a GET request, so AEM does not strictly treat it as a POST and try to modify the node /content/eaem-cs-spa-read-post-data/us/en/home

package apps.experienceaem.sites.spa.core.filters;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM convert SPA home requests from POST to GET ",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=(/content/eaem-cs-spa-read-post-data/us/en/home*)",
}
)
public class OnBoardingPostModifierFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(OnBoardingPostModifierFilter.class);

@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;
SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;

try {
if(!slingRequest.getMethod().equals("POST")){
chain.doFilter(request, response);
return;
}

slingResponse.setHeader("Dispatcher", "no-cache");

RequestDispatcher dp = request.getRequestDispatcher(slingRequest.getRequestPathInfo().getResourcePath() + ".html");

dp.include(new GetSlingServletRequestWrapper(slingRequest), response);
} catch (Exception e) {
log.error("Error converting POST to GET of SPA home : " + slingRequest.getRequestURI());
}
}

@Override
public void destroy() {
}

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

public String getMethod() {
return "GET";
}
}
}


6) For the SPA App to read POSTed form data from a JS object added in window.eaemInitialData create a model apps.experienceaem.sites.spa.core.models.PostParamsModel

package apps.experienceaem.sites.spa.core.models;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

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

@Inject
SlingHttpServletRequest request;

private String eaemInitialData;

@PostConstruct
protected void init() {
String eaemName = request.getParameter("eaemName");
String eaemEmail = request.getParameter("eaemEmail");

JSONObject jsonObject = new JSONObject();

try{
jsonObject.put("eaemName", eaemName);
jsonObject.put("eaemEmail", eaemEmail);
}catch (Exception e){
log.error("Error creating eaemInitialData from request",e);
}

eaemInitialData = jsonObject.toString();
}

/**
* @return brand
*/
public String getEaemInitialData() {
return eaemInitialData;
}
}


7) Initialize window.eaemInitialData object with POSTed form data in SPA App root page eaem-cs-spa-read-post-data\ui.apps\src\main\content\jcr_root\apps\eaem-cs-spa-read-post-data\components\page\body.html

<noscript>You need to enable JavaScript to run this app.</noscript>
<script data-sly-use.model="apps.experienceaem.sites.spa.core.models.PostParamsModel" data-sly-test="${model.eaemInitialData}">
window.eaemInitialData = ${model.eaemInitialData @ context='unsafe'};
</script>
<div id="spa-root"></div>


9) Crate a sling model exporter apps.experienceaem.sites.spa.core.models.EAEMGenericComponentSlingExporter for the Steps component...

package apps.experienceaem.sites.spa.core.models;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.Map;

@Model(
adaptables = {SlingHttpServletRequest.class},
adapters = {ComponentExporter.class},
resourceType = {
"eaem-cs-spa-read-post-data/components/image",
"eaem-cs-spa-read-post-data/components/steps"
}
)
@Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class EAEMGenericComponentSlingExporter implements ComponentExporter {

@Inject
private Resource resource;

@PostConstruct
protected void initModel() {
}

public ValueMap getEaemData(){
return resource.getValueMap();
}

@Override
public String getExportedType() {
return resource.getResourceType();
}
}


8) Create the React render script eaem-cs-spa-read-post-data\ui.frontend\src\components\Steps\Steps.tsx for component /apps/eaem-cs-spa-read-post-data/components/steps. It reads the data entered in lead form entry from window.eaemInitialData

import { MapTo } from "@adobe/aem-react-editable-components";
import React, { FC, useState, useEffect } from "react";
import {Link} from "react-router-dom";
import "./StepsStyles.css";

type StepsProps = {
[x: string]: any
};

declare global {
interface Window {
eaemInitialData: any;
}
}

const StepsEditConfig = {
emptyLabel: "Steps - Experience AEM",

isEmpty: function (props: any) {
return !props || !props["jcr:title"];
}
};

const AEMSteps: FC<StepsProps> = props => {
const eaemInitialData = window.eaemInitialData;

return (
<div>
<h1 style={{ textAlign: "center", color: "maroon" }}>
{props["jcr:title"]}
</h1>

{
props.showName &&
<div className='eaem-info'>
<span>Enter name</span>
<input name='eaemName' value={eaemInitialData.eaemName}></input>
</div>
}
{
props.showEmail &&
<div className='eaem-info'>
<span>Enter email</span>
<input name='eaemEmail' value={eaemInitialData.eaemEmail}></input>
</div>
}
{
props.showSSN &&
<div className='eaem-info'>
<span>Enter SSN</span>
<input name='eaemSSN' value={eaemInitialData.eaemSSN}></input>
</div>
}
{
props.showCompany &&
<div className='eaem-info'>
<span>Enter company</span>
<input name='eaemCompany' value={eaemInitialData.eaemCompany}></input>
</div>
}

<div className='eaem-info'>
{
props.previousLink &&
<Link to={props.previousLink}>
<button type="button">Previous</button>
</Link>
}
{
props.nextLink &&
<Link to={props.nextLink}>
<button type="button">Next</button>
</Link>
}
</div>
</div>
);
};

export default MapTo("eaem-cs-spa-read-post-data/components/steps")(AEMSteps, StepsEditConfig);



AEM Cloud Service - Sample OAuth Authentication using Apache Httpclient

$
0
0

Goal

Sample code to make a connection to OAuth Identity Server, authenticate and get the Bearer token...


Service Interface

package app.eaem.api.core.services;

import org.json.JSONObject;

public interface EAEMOAuthService {
public String getBearerToken() throws Exception;

public JSONObject getPhotoNames();
}


Service Implementation

package app.eaem.api.core.services.impl;

import app.eaem.api.core.services.EAEMOAuthService;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Form;
import org.apache.http.client.fluent.Request;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
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 java.util.Date;

@Component(service = EAEMOAuthService.class)
@Designate(ocd = EAEMOAuthServiceImpl.EAEMOAuthConfiguration.class)
public class EAEMOAuthServiceImpl implements EAEMOAuthService {
private static final Logger log = LoggerFactory.getLogger(EAEMOAuthServiceImpl.class);

private String eaemIdentityServerUrl = "";
private String clientId = "";
private String clientSecret = "";
private String scope = "";
private String eaemAppUrl = "";

private String bearerToken = "";
private long tokenExpiryDate = 0;

@Reference
private transient HttpClientBuilderFactory httpClientBuilderFactory;

private transient CloseableHttpClient httpClient;

@Activate
@Modified
protected void activate(final EAEMOAuthConfiguration config) {
eaemIdentityServerUrl = config.eaem_identity_server_url();
clientId = config.eaem_client_id();
clientSecret = config.eaem_client_secret();
scope = config.eaem_scope();
eaemAppUrl = config.eaem_app_url();

final HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();

final RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000)
.setSocketTimeout(30000).build();

builder.setDefaultRequestConfig(requestConfig);

httpClient = builder.build();
}

public JSONObject getPhotoNames(){
JSONObject photos = new JSONObject();

try{
photos = new JSONObject(makeRequest(eaemAppUrl + "/api/Photos"));
}catch(Exception e){
log.error("Error getting leads", e);
}

return photos;
}

public String getBearerToken() throws Exception{
long rightNow = new Date().getTime();

if(StringUtils.isNotEmpty(bearerToken) && (rightNow < tokenExpiryDate)){
return bearerToken;
}

Form form = Form.form();
form.add("grant_type", "client_credentials");
form.add("client_id", clientId);
form.add("client_secret", clientSecret);
form.add("scope", scope);

String authResponse = Request.Post(eaemIdentityServerUrl)
.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.bodyForm(form.build()).execute().returnContent().asString();

if(StringUtils.isEmpty(authResponse)){
throw new Exception("Error authenticating with id and secret");
}

JSONObject bearerObj = new JSONObject(authResponse);
bearerToken = bearerObj.getString("access_token");
tokenExpiryDate = new Date().getTime() + (bearerObj.getInt("expires_in") * 1000);

return bearerToken;
}

private String makeRequest(String uri) throws Exception{
return Request.Get(uri)
.addHeader("Authorization", "Bearer " + getBearerToken())
.execute().returnContent().asString();
}

@ObjectClassDefinition(name = "OAuth EAEM Configuration")
public @interface EAEMOAuthConfiguration {

@AttributeDefinition(
name = "EAEM Identity Server Url",
description = "EAEM Identity Server Url",
defaultValue = "https://eaem-idsvr-dev.somehost.com/connect/token",
type = AttributeType.STRING)
String eaem_identity_server_url();

@AttributeDefinition(
name = "EAEM App URL",
description = "EAEM App URL",
defaultValue = "https://eaem-app.somehost.com",
type = AttributeType.STRING)
String eaem_app_url();

@AttributeDefinition(
name = "EAEM Client Id",
description = "EAEM Client Id",
type = AttributeType.STRING)
String eaem_client_id();

@AttributeDefinition(
name = "EAEM Client Secret",
description = "EAEM Client Secret",
type = AttributeType.STRING)
String eaem_client_secret();

@AttributeDefinition(
name = "EAEM App Scope",
description = "EAEM App Scope",
defaultValue = "photos:eaemorg",
type = AttributeType.STRING)
String eaem_scope();
}
}

AEM 6560 - React SPA Text component with Material UI Theme and Styles

$
0
0

Goal

Add AEM SPA React Text component showing paragraph texts created using Typescript and Material UI (MUI). Using MUI there are no global style-sheets, each component is independent, so there are no css conflicts at page level polluting global scope. In the following steps a MUI theme created with device specific breakpoints shows AdobeCaslonPro font and device specific font sizes...

Package Install | Github


Typography in Sketch File (opened using Windows Lunacy App)



Typography in AEM


Solution


1) Create the project structure (for both React SPA and MPA authoring) with the following command using maven archetype - https://github.com/adobe/aem-project-archetype

mvn -B archetype:generate -D archetypeGroupId=com.adobe.granite.archetypes -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=23 -D aemVersion=6.5.0 -D appTitle="Experience AEM SPA React" -D appId="eaem-sites-react-spa-material-ui-text" -D groupId="com.eaem"
-D frontendModule=react -D includeExamples=n -D includeErrorHandler=n -D includeDispatcherConfig=n

2) Remove all additional components created, except the following required for testing... (or download Package Install)

                                                          /apps/eaem-sites-react-spa-material-ui-text/components/spa
                                                          /apps/eaem-sites-react-spa-material-ui-text/components/page
                                                          /apps/eaem-sites-react-spa-material-ui-text/components/text

3) Open a command prompt (terminal) at eaem-sites-react-spa-material-ui-text\ui.frontend and install the typscript and material ui specific dependencies

                                                          npm install typescript
                                                          npm install @material-ui/core
                                                          npm install classnames

4) Create the component /apps/eaem-sites-react-spa-material-ui-text/components/text. In the next step we'd be creating the react render type script...

5) Add the component render script in eaem-sites-react-spa-material-ui-text\ui.frontend\src\components\AEMText\AEMText.tsx with the following code...

import { MapTo } from "@adobe/cq-react-editable-components";
import React, { FC, useState, useEffect } from "react";
import {
makeStyles, Theme, createStyles
} from "@material-ui/core";
import { createMuiTheme } from "@material-ui/core/styles";
import createBreakpoints from "@material-ui/core/styles/createBreakpoints";

type TextProps = {
cqPath: string;
text: string;
};

const AEMTextEditConfig = {
emptyLabel: "Text - Experience AEM",

isEmpty: function (props: any) {
return !props || !props.text || props.text.trim().length < 1;
}
};

function extractModelId(path: string) {
return path && path.replace(/\/|:/g, "_");
}

enum BREAKPOINTS {
XS = 0,
SM = 768,
MD = 992,
LG = 1200,
XL = 1600
}

const eaemTheme = createMuiTheme({
breakpoints: createBreakpoints({
values: {
xs: BREAKPOINTS.XS,
sm: BREAKPOINTS.SM,
md: BREAKPOINTS.MD,
lg: BREAKPOINTS.LG,
xl: BREAKPOINTS.XL
},
get down() {
return (key : number | string) => {
let values = this.values as any;
return `@media (max-width: ${values[key]- 0.05}px)`;
}
},
get between() {
return (a : number | string, b : number | string) => {
let values = this.values as any;
return `@media (min-width:${values[a]}px) and (max-width:${values[b] - 0.05}px)`;
}
}
})
});

const useStyles = makeStyles(() => {
console.log(eaemTheme.breakpoints.up("md"));

return createStyles({
root: {
fontFamily: 'AdobeCaslonPro, Times, serif !important',
'& h1': {
[eaemTheme.breakpoints.down("xl")]: {
fontSize: '34px',
},
[eaemTheme.breakpoints.down("lg")]: {
fontSize: '30px',
},
[eaemTheme.breakpoints.down("md")]: {
fontSize: '26px',
}
},
'& h2': {
[eaemTheme.breakpoints.down("xl")]: {
fontSize: '28px',
},
[eaemTheme.breakpoints.down("lg")]: {
fontSize: '25px',
},
[eaemTheme.breakpoints.down("md")]: {
fontSize: '22px',
}
},
'& h3': {
[eaemTheme.breakpoints.down("xl")]: {
fontSize: '22px',
},
[eaemTheme.breakpoints.down("lg")]: {
fontSize: '20px',
},
[eaemTheme.breakpoints.down("md")]: {
fontSize: '18px',
}
},
'& p': {
fontSize: '13px',
},
"& a:hover": {
textDecoration: "none"
}
}
})
});

const AEMText: FC<TextProps> = props => {
const classes = useStyles();

return (
<div
className={classes.root}
id={extractModelId(props.cqPath)}
data-rte-editelement
dangerouslySetInnerHTML={{
__html: props.text
}}
/>
);
};

export default MapTo("eaem-sites-spa-how-to-react/components/text")(AEMText, AEMTextEditConfig);

6) Add AEMText.tsx path in eaem-sites-react-spa-material-ui-text\ui.frontend\src\components\import-components.js

                                                          import './Page/Page';
                                                          import './AEMText/AEMText';
                                                          import './Title/Title';
                                                          import './Nav/Nav';
                                                          import './Image/Image';

AEM Cloud Service - Assets Metadata Editor Multifield Limit Validation

$
0
0

Goal

Add a Client Side Validator for limiting the number of items in a Multifield in Assets Metadata Editor

The min/max limit configuration is hardcoded in the extension. For making it configurable via schema model check this post

Demo | Package Install | Github



Solution

Create a client library /apps/eaem-cs-meta-limit-mf/clientlibs/clientlib-limit-mf with categories=dam.gui.coral.metadataeditor and dependencieseaem.lodash. Add the following js logic for validator in /apps/eaem-cs-meta-limit-mf/clientlibs/clientlib-limit-mf/meta-limit-mf.js

(function($, $document){
var DATA_MF_NAME = "data-granite-coral-multifield-name",
MF_NAME = "./jcr:content/metadata/eaemKeywords",
EAEM_VALIDATION_ERROR = "eaem-assets-validation-error",
EAEM_MAX_ITEMS = 5,
EAEM_MIN_ITEMS = 1, continueSave = true;

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

function validateMF(){
var $multifield = $("[" + DATA_MF_NAME + "='" + MF_NAME + "']");

addValidator($multifield, EAEM_MAX_ITEMS, EAEM_MIN_ITEMS);

var validation = $multifield.adaptTo("foundation-validation");

validation.checkValidity();

validation.updateUI();

$("#shell-propertiespage-doneactivator").click(function(event){
if(continueSave){
return;
}

event.preventDefault();
event.stopPropagation();

showAlert("error", "Error", "Validation failed");
})
}

function addValidator($multifield, maxItems, minItems){
if(maxItems){
maxItems = parseInt(maxItems);
}

if(minItems){
minItems = parseInt(minItems);
}

$multifield.attr("aria-required","true");

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

registry.register("foundation.validation.validator", {
selector: "#" + $multifield.attr("id"),
validate: validate
});

function validate(){
var count = $multifield[0]._items.length;

continueSave = true;

if(maxItems && (count > maxItems) ){
continueSave = false;
return "Maximum allowed : " + maxItems + " items";
}

if(minItems && (count < minItems) ){
continueSave = false;
return "Minimum required : " + minItems + " items";
}

return false;
}
}

function showAlert(variant, header, content) {
var $dialog = $("#" + EAEM_VALIDATION_ERROR), dialog;

if (!_.isEmpty($dialog)) {
$dialog[0].show();
return;
}

dialog = new Coral.Dialog().set({
id: EAEM_VALIDATION_ERROR,
variant: variant,
closable: "on",
header: {
innerHTML: header
},
content: {
innerHTML: content
},
footer: {
innerHTML: '<button is="coral-button" variant="default" coral-close>OK</button>'
}
});

document.body.appendChild(dialog);

dialog.show();
}
}(jQuery, jQuery(document)));



AEM Cloud Service - Sample Java Standalone Program to Upload Assets

$
0
0

Goal

AEM Cloud Version : 2021.6.5382.20210602T190018Z-210527 (June 2, 2021)

Uploading assets to AEM Cloud Service is a 3 step process (initiateUpload.json, Upload to blob storage, completeUpload.json). Tools like https://github.com/adobe/aem-upload can be used to upload files to AEM using a NodeJS client. 

Drew Robinson explains the steps in detail here

Demo | Github


Command

Add aem-sdk-api-2021.5.5343.20210524T070738Z-210527.jar to your class path and run the program apps.UploadToAEMCS

java -classpath "C:\dev\projects\aemcs\out\production\test;C:\Users\nalabotu\.m2\repository\com\adobe\aem\aem-sdk-api\2021.5.5343.20210524T070738Z-210527\aem-sdk-api-2021.5.5343.20210524T070738Z-210527.jar" apps.UploadToAEMCS


Program

1) Get the Bearer Token from AEM Developer Console



2) Set the AEM CS host name, AEM folder path, Bearer token and input file path in the following java stand alone program to perform the 3 steps (initiateUpload.json, Upload to Blob Storage, completeUpload.json). For uploading a big file the binary has to be split into multiple chunks and uploaded using various PUT urls provided by the initiateUpload.json response


package apps;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;

import org.apache.http.client.fluent.Form;
import org.apache.http.client.fluent.Request;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;

public class UploadToAEMCS {
public static String AEM_CS_HOST = "https://author-p10961-e90064.adobeaemcloud.com";
public static String AEM_FOLDER = "/content/dam/experience-aem";
public static String INPUT_FILE = "C:/Users/nalabotu/Pictures/adobe.jpg";
public static String BEARER_TOKEN = "eyJhbGci................";

public static String INITIATE_UPLOAD_REQ = AEM_CS_HOST + AEM_FOLDER + ".initiateUpload.json";
public static String FILE_NAME = "fileName";
public static String UPLOAD_TOKEN = "uploadToken";
public static String MIME_TYPE = "mimeType";
public static String FILE_SIZE = "fileSize";

public static void main(String[] args) throws Exception {
String initiateUploadResponse = makeInitiateUploadRequest();

JSONObject uploadResponse = new JSONObject(initiateUploadResponse);
JSONArray filesJSON = uploadResponse.getJSONArray("files");
JSONObject fileJSON = (JSONObject) filesJSON.get(0);

FileInputStream fileIn = new FileInputStream(INPUT_FILE);
byte[] fileBytes = IOUtils.toByteArray(fileIn);

String binaryPUTUrl = fileJSON.getJSONArray("uploadURIs").getString(0);
HttpResponse putResponse = Request.Put(binaryPUTUrl)
.bodyByteArray(fileBytes).execute().returnResponse();

int statusCode = putResponse.getStatusLine().getStatusCode();

if( (statusCode < 200) || (statusCode > 210)){
throw new Exception("Error uploading the binary");
}

String completeResponse = makeCompleteUploadRequest(uploadResponse);

System.out.println("Uploaded : " + (AEM_FOLDER + completeResponse) );
}

private static String makeInitiateUploadRequest()throws Exception{
File inputFile = new File(INPUT_FILE);

Form form = Form.form();
form.add(FILE_NAME, inputFile.getName());
form.add(FILE_SIZE, String.valueOf(inputFile.length()));

String initiateUploadResponse = Request.Post(INITIATE_UPLOAD_REQ)
.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.addHeader("Authorization", "Bearer " + BEARER_TOKEN)
.bodyForm(form.build()).execute().returnContent().asString();

System.out.println(initiateUploadResponse);

return initiateUploadResponse;
}

private static String makeCompleteUploadRequest(JSONObject uploadResponse) throws Exception{
JSONArray filesJSON = uploadResponse.getJSONArray("files");
JSONObject fileJSON = (JSONObject) filesJSON.get(0);

String uploadToken = fileJSON.getString("uploadToken");
String mimeType = fileJSON.getString("mimeType");

String completeURI = AEM_CS_HOST + uploadResponse.getString("completeURI");
File inputFile = new File(INPUT_FILE);

Form form = Form.form();
form.add(UPLOAD_TOKEN, uploadToken);
form.add(FILE_NAME, inputFile.getName());
form.add(MIME_TYPE, mimeType);

String completeResponse = Request.Post(completeURI)
.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.addHeader("Authorization", "Bearer " + BEARER_TOKEN)
.bodyForm(form.build()).execute().returnContent().asString();

return completeResponse;
}
}




AEM Cloud Service - Create Adobe Target Offers as AEM Experience Fragments for Non AEM Sites

$
0
0

Goal

Use AEM as Offer Server where the offers (with text and images) are created by marketing team as XFs (Experience Fragments) in Authoring and Published to AEM Publish. XF editor provides an easy to use interface for creating html fragments. For personalizing the offers we use Adobe Target Activities and XF Offers. Finally render the Personalized offer in a Third-party website (not running in AEM Publish). The offer html and necessary images are accessed by the third party website from AEM Publish

Demo | Package Install | Github



Setup IMS in AEM for Target Integration

                    https://experienceleague.adobe.com/docs/experience-manager-65/administering/integration/integration-ims-adobe-io.html

                    Tools> Security> Adobe IMS Configurations


                    1. Create a Certificate in AEM...




                    2. Upload Certificate in https://console.adobe.io




                    3. Continue the IMS integration in AEM...



 


                    4. Provide the necessary permissions for Integration in Admin Console. Without this you might see the following error when exporting XFs to Target...

Caused by: com.day.cq.analytics.testandtarget.impl.service.WebServiceException: Unexpected response status code [403] for request [https://mc.adobe.io/ags959/target/offers/content?includeMarketingCloudMetadata=true].{"httpStatus":403,"requestId":"SKeC0FKYUiWHkDxi2D47KS33xMkRKpdy","requestTime":"2021-06-15T17:12:51.320578Z","errors":[{"errorCode":"Forbidden.Resource","message":"Access denied. To perform this operation, all of the following privileges are required \"[editor]\".","meta":{}}]}at com.day.cq.analytics.testandtarget.impl.service.WebServiceImpl.request(WebServiceImpl.java:610) [com.adobe.cq.cq-target-integration:1.4.30]... 163 common frames omitted




Create Target Integration using IMS

                    Tools> Cloud Services> Legacy Cloud Services> Adobe Target



Create XF Components in AEM

1) Create a Text component in AEM extending Core Text Component (sling:resourceSuperType="core/wcm/components/text/v2/text")

2) Create an Image component in AEM extending Core Image Component (sling:resourceSuperType="core/wcm/components/image/v2/image"), add the following render script in /apps/eaem-cs-at-json-offer/components/image/image.html

<div data-sly-use.image="apps.experienceaem.assets.core.filters.ImageModel"
data-sly-test="${image.src}"
style="text-align:center">
<img src="${image.src}" style="margin-left: auto; margin-right: auto;"/>
</div>

<div data-sly-use.image="apps.experienceaem.assets.core.filters.ImageModel"
style="text-align: center; padding: 30px"
data-sly-test="${!image.src && wcmmode.edit}">
Image not set
</div>


3) Create the model apps.experienceaem.assets.core.filters.ImageModel with following code for the Image component render script (this is for generating the full image link using Externalizer)

package apps.experienceaem.assets.core.filters;

import com.day.cq.commons.Externalizer;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

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

@Inject
SlingHttpServletRequest request;

@Inject
private SlingSettingsService slingSettingsService;

@ValueMapValue
@Optional
private String fileReference;

private String src = "";

@PostConstruct
protected void init() {
if(StringUtils.isEmpty(fileReference)){
return;
}

SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
ResourceResolver resolver = slingRequest.getResourceResolver();

Externalizer externalizer = resolver.adaptTo(Externalizer.class);
boolean isAuthor = slingSettingsService.getRunModes().contains(Externalizer.AUTHOR);

src = !isAuthor ? externalizer.publishLink(resolver, fileReference) : externalizer.authorLink(resolver, fileReference);
}

public String getSrc(){
return src;
}
}


4) Use the Web Variation template /conf/eaem-cs-at-json-offer/settings/wcm/templates/xf-web-variation created by maven archetype command...

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=24 
-D aemVersion=cloud -D appTitle="Experience AEM Target JSON Offer" -D appId="eaem-cs-at-json-offer" -D groupId="apps.experienceaem.assets"
-D frontendModule=none -D includeExamples=n -D includeDispatcherConfig=y


5) Create a filter apps.experienceaem.assets.core.filters.ExperienceFragmentJSONOfferFilter with the following code to modify the product model.json response, so it just provides the publish url of XF...

eg. https://publish-p10961-e90064.adobeaemcloud.com/content/experience-fragments/eaem-cs-at-json-offer/us/en/site/eaem-cs-three/master.model.json

{

}

package apps.experienceaem.assets.core.filters;

import com.day.cq.commons.Externalizer;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM - Change offer JSON exported to Target",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=(/content/experience-fragments/.*.model.json)",
}
)
public class ExperienceFragmentJSONOfferFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(ExperienceFragmentJSONOfferFilter.class);

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

try {
String uri = slingRequest.getRequestURI();

if(!uri.endsWith(".model.json")){
chain.doFilter(request, response);
return;
}

JSONObject model = new JSONObject();

String masterXFPath = uri.substring(0,uri.lastIndexOf(".model.json"));

masterXFPath = masterXFPath + ".html";

ResourceResolver resolver = slingRequest.getResourceResolver();
Externalizer externalizer = resolver.adaptTo(Externalizer.class);

model.put("xfHtmlPath", externalizer.publishLink(resolver, masterXFPath));

response.getWriter().print(model);
} catch (Exception e) {
log.error("Error getting json offer response : " + slingRequest.getRequestURI());
}
}

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

@Override
public void destroy() {
}
}


6) Set the Adobe Target configuration and Export Format on the XF folder eg. https://author-p10961-e90064.adobeaemcloud.com/mnt/overlay/cq/experience-fragments/content/experience-fragments/folderproperties.html/content/experience-fragments/eaem-cs-at-json-offer/us/en/site



CORS and Dispatcher Changes

1) Browsers do not allow cross origin requests unless the Access-Control-Allow-Origin header is present in response headers. So for the third party website (for this post its a local file C:\.....\eaem-cloud-service\eaem-cs-at-json-offer\scripts\show-xf-offer\show-aem-xf-json-offer.html) to make a CORS call to AEM publish (eg. https://publish-p10961-e90064.adobeaemcloud.com) for loading the XF offer html, some configuration changes are required. For more info check adobe documentation. When Access-Control-Allow-Origin header is NOT present in the response you might see the following error...

Access to XMLHttpRequest at 'http://localhost:8080/content/experience-fragments/eaem-cs-at-json-offer/us/en/site/test/master.html' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.


2) Add the CORS configuration file ui.config\src\main\content\jcr_root\apps\eaem-cs-at-json-offer\osgiconfig\config\com.adobe.granite.cors.impl.CORSPolicyImpl~eaem-cs-at-json-offer.cfg.json with following settings (adding it in config.publish folder should be fine if the XF html is delivered only by publish instance or just adding the header it Dispatcher, discussed in next section, should be fine... however during debugging when Dispatcher has to be bypassed you might find the need for this configuration in AEM Author and Publish instances)

{
"supportscredentials": false,
"exposedheaders": [
"*"
],
"supportedmethods": [
"GET",
"HEAD",
"POST"
],
"alloworigin": [
""
],
"maxage:Integer": 1800,
"alloworiginregexp": [
".*"
],
"allowedpaths": [
".*"
],
"supportedheaders": [
"Origin",
"Accept",
"X-Requested-With",
"Content-Type",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Authorization"
]
}


3) In the above step alloworiginregexp was set to ".*". This will set the access-control-allow-origin to wildcard * in response headers allowing any cross origin request (in real scenarios you may want to set it to the third party website domain name). If your third party website is a local file, chrome sends null origin header so setting alloworigin to null or alloworiginregexp to .* works...




4) The next step is configuring Dispatcher to allow this header in response (Publish instance sends the header but unless cleared via configuration, Dispatcher blocks it)


5) Create a copy of eaem-cs-at-json-offer\dispatcher\src\conf.d\available_vhosts\default.vhost and name it eaem-cs-at-json-offer\dispatcher\src\conf.d\available_vhosts\eaem.vhost. Add the access control allow origin header configuration...

<IfModule mod_headers.c>
Header add X-Vhost "publish"
Header add Access-Control-Allow-Origin "*"
</IfModule>


6) For caching the Access-Control-Allow-Origin response header, create a copy of eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\available_farms\default.farm and name it eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\available_farms\eaem.farm. Add the access control allow origin header cache configuration...

/headers {
"Cache-Control"
"Content-Disposition"
"Content-Type"
"Expires"
"Last-Modified"
"X-Content-Type-Options"
"Access-Control-Allow-Origin"
}


7) You need to create SYMLINK eaem-cs-at-json-offer\dispatcher\src\conf.d\enabled_vhosts\eaem.vhost and eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\enabled_farms\eaem.farm pointing to the respective eaem.vhost and eaem.farms created in steps above. This is tricky in Windows OS, use command prompt, run as administrator and create them using following commands... (for more details on various ways to create the symlinks check this post)

> git config --global core.symlinks true

> cd C:\dev\projects\eaem-cloud-service\code\dispatcher\src\conf.d\enabled_vhosts

> mklink  eaem.vhost "../available_vhosts/eaem.vhost"

> cd C:\dev\projects\eaem-cloud-service\code\dispatcher\src\conf.dispatcher.d\enabled_farms

> mklink  eaem.farm "../available_farms/eaem.farm"




8) You can optionally test these dispatcher changes using the dispatcher tools locally (check documentation). Testing your dispatcher changes locally first is productive, as the CS build pipelines take time and any errors in dispatcher config fail the pipeline...


9) After unzipping the Dispatcher tools eg. aem-sdk-dispatcher-tools-x.x.x-windows.zip , run the following commands to validate dispatcher config changes and test it using a Docker container (assuming you have Docker Desktop installed and the Publish instance is started on 4503)

> dispatcher-tools\bin\validator full -f -d out src

> dispatcher-tools\bin\docker_run out host.docker.internal:4503 8080


10) If everything goes well the XF is available for viewing on...





11) Sometimes when starting Docker you might see the following error. To workaround open Windows Services (Run > services.msc) and restart the service named Hyper-V Virtual Machine Management, restart Docker...

System.InvalidOperationException: Failed to deploy distro docker-desktop to C:\Users\nalabotu\AppData\Local\Docker\wsl\distro: exit code: -1  stdout: Logon failure: the user has not been granted the requested logon type at this computer.





Exporting to Adobe Target

1) AEM Cloud services is all setup up with the XF related code and configuration. Lets export the created XF to Target...


2) Login to https://experience.adobe.com using your Adobe ID (make sure you are in the right org) and click on Target icon...


3) Click on Offers icon and you should see the exported offers from AEM...



Create Target Activities 

1) Lets create a personalized A/B Test Activity to use the XF Offers exported from AEM...



2) Select the XF offer for Experience A. The mbox name here can be any eg. eaem-cs-test-3 and  the same name is used when loading the activity in third party website. Only the AEM Publish url of XF offer is exported from AEM, as you can see in the screenshot below...



3) Select the XF offer for Experience B



4) Personalization used here is a random 50:50 delivery of the experiences A and B...



5) Finish the Activity creation by setting the conversion goal as Viewed an mbox


6) Make the Activity Live...



Offer loading Script in Third Party

1) For this post we are using a local file (C:\.....\eaem-cloud-service\eaem-cs-at-json-offer\scripts\show-xf-offer\show-aem-xf-json-offer.html) as the third-party website showing XF offers. Integrating Target with Launch is the right way to do it (check documentation) however to keep things simple download the Target lib file at.js from your Adobe Target account...



2) Add at.js and jquery-3.6.0.min.js in your thirdparty script to make a connection to target, get the personalized offer JSON with AEM Publish url and further get the html content of the XF from AEM Publish to show in a div...

<!--https://experienceleague.adobe.com/docs/target/using/implement-target/client-side/at-js-implementation/deploy-at-js/implementing-target-without-a-tag-manager.html?lang=en#task_E85D2F64FEB84201A594F2288FABF053-->
<!doctype html>
<html>

<head>
<meta charset="utf-8">
<title>Experience AEM XF Target Offer</title>

<!--Preconnect and DNS-Prefetch to improve page load time-->
<link rel="preconnect" href="//ags959.tt.omtrdc.net?lang=en">
<link rel="dns-prefetch" href="//ags959.tt.omtrdc.net?lang=en">
<!--/Preconnect and DNS-Prefetch-->

<!--jQuery or other helper libraries should be implemented before at.js if you would like to use their methods in Target-->
<script src="./js/jquery-3.6.0.min.js"></script>
<!--/jQuery-->

<!--Target's JavaScript SDK, at.js-->
<script src="./js/at.js"></script>
<!--/at.js-->
</head>

<body>
<div style="text-align: center; margin-top: 20px">
This pages loads AEM Experience Fragment Offers (authored as JSON and rendered as HTML)
</div>

<div id="aem-offer" style="text-align: center; margin-top: 30px">
Default content before HTML; Loading the XF offer...
</div>

<script>
function loadOffer() {
var $aemOffer = $("#aem-offer");

adobe.target.getOffer({
mbox: 'eaem-cs-test-3',
success: function (offer) {
var offerJSON = JSON.parse(offer[0].content);

if (!offerJSON.xfHtmlPath) {
$aemOffer.html("Error loading offer");
return;
}

$.ajax(offerJSON.xfHtmlPath).done(function (html) {
$aemOffer.html(html);
});

}, error: function () {
$aemOffer.html("Target Error loading offer");
}
})
}

setTimeout(loadOffer, 3000);
</script>
</body>

</html>


3) The offer rendered in thirdparty page...



AEM Cloud Service - Assets Processing Profile and Post Processing workflow to generate JPEG Assets

$
0
0

Goal

Create Low res JPEG images from High quality Master assets (eg. psd, tif) uploaded to AEM, so the jpeg images can be browsed using AEM Desktop app and placed in 3rd party apps like Figma.

Demo | Package Install | Github



Solution

1) Create a Process Step apps.experienceaem.assets.core.workfows.ConvertRenditionToAssetProcess used in a Post Processing workflow for creating the JPEG asset from rendition added by a Processing Profile. It's worth noting that streaming of binaries in AEM cloud services is not considered a best practice, the rendition.getBinary() call in below code does not stream binary but gets a pointer to the binary in blob store. Also when processing the created asset set runPostProcess to false so the post processing workflow does not run on generated assets...

package apps.experienceaem.assets.core.workfows;

import com.adobe.granite.asset.api.AssetRelation;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowData;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.AssetManager;
import com.day.cq.dam.api.Rendition;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.mime.MimeTypeService;
import org.apache.sling.engine.SlingRequestProcessor;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

@Component(
service = WorkflowProcess.class,
property = { "process.label=Experience AEM Convert Rendition to Asset, Workflow Process Step" }
)
public class ConvertRenditionToAssetProcess implements WorkflowProcess {
private static final Logger log = LoggerFactory.getLogger(ConvertRenditionToAssetProcess.class);

private static final String RELATION_SOURCES = "sources";
private static final String RELATION_DERIVED = "derived";
private static final String DAM_DERIVED_ASSET = "dam:derivedAsset";
private static final String PROCESS_ASSET_SERVLET = "/bin/asynccommand";
private static final String PROCESS_ASSET_OPERATION = "PROCESS";
private static final String PROCESS_ASSET_PROFILE = "full-process";

private static String FIGMA_REND_NAME = "figma";

@Reference
private MimeTypeService mimeTypeService;

@Reference
private SlingRequestProcessor slingRequestProcessor;

@Reference
private RequestResponseFactory requestResponseFactory;

public void execute(final WorkItem workItem, final WorkflowSession workflowSession, final MetaDataMap args)
throws WorkflowException {
String assetPath = getPayloadPath(workItem.getWorkflowData());

try{
ResourceResolver resolver = workflowSession.adaptTo(ResourceResolver.class);

Resource assetRes = resolver.getResource(assetPath);

if(assetRes == null){
log.warn("Resource not found - " + assetRes);
return;
}

String assetNameNoExt = assetRes.getName().substring(0, assetRes.getName().lastIndexOf("."));

if(assetNameNoExt.endsWith(("-") + FIGMA_REND_NAME)){
log.warn("Dont create Figma assets out of rendition for - " + assetPath);
return;
}

Asset asset = assetRes.adaptTo(Asset.class);

Rendition rendition = asset.getRendition(FIGMA_REND_NAME + ".jpeg");

if (rendition == null) {
log.warn("Unable to get rendition " + FIGMA_REND_NAME + " of asset " + assetPath);
return;
}

Asset derivedAsset = createFigmaAsset(resolver, assetRes, rendition);

if(derivedAsset == null){
log.warn("Error creating Figma asset for : " + assetPath);
return;
}

resolver.commit();

processNewAsset(resolver, derivedAsset.getPath());
}catch(Exception e){
log.error("Error occured while converting rendtion to asset for payload - " + assetPath, e);
}
}

private Asset createFigmaAsset(ResourceResolver resolver, Resource assetRes, Rendition rendition){
Asset asset = assetRes.adaptTo(Asset.class);

String assetNameNoExt = assetRes.getName().substring(0, assetRes.getName().lastIndexOf("."));

String newAssetPath = createAssetPath(assetRes.getParent(), assetNameNoExt, rendition.getName());

String mimeType = mimeTypeService.getMimeType(rendition.getName());

AssetManager assetManager = resolver.adaptTo(AssetManager.class);

Asset derivedAsset = assetManager.createOrReplaceAsset(newAssetPath, rendition.getBinary(), mimeType, false);

if (derivedAsset == null) {
return null;
}

addRelation(derivedAsset, RELATION_SOURCES, asset.getPath());

addRelation(asset, RELATION_DERIVED, newAssetPath);

return derivedAsset;
}

private void processNewAsset(ResourceResolver resolver, String assetPath){
log.info("Starting Reprocess of Asset : " + assetPath);

Map<String, Object> requestParams = new HashMap<String, Object>();
requestParams.put("operation", PROCESS_ASSET_OPERATION);
requestParams.put("profile-select", PROCESS_ASSET_PROFILE);
requestParams.put("runPostProcess", "false"); // donot run the post process, might resulting in a processing loop
requestParams.put("description", "Processing created figma asset - " + assetPath);
requestParams.put("asset", assetPath);

HttpServletRequest request = requestResponseFactory.createRequest("POST", PROCESS_ASSET_SERVLET, requestParams);
ByteArrayOutputStream bos = new ByteArrayOutputStream();

HttpServletResponse response = this.requestResponseFactory.createResponse(bos);

try {
slingRequestProcessor.processRequest(request, response, resolver);
}catch (Exception e){
log.error("Error occured while processing the new figma asset - " + assetPath);
}
}


private void addRelation(Asset asset, String name, String relatedAssetPath) {
com.adobe.granite.asset.api.Asset graniteAsset = asset.adaptTo(com.adobe.granite.asset.api.Asset.class);
Iterator<? extends AssetRelation> relationItr = graniteAsset.listRelations(name);

while(relationItr.hasNext()) {
AssetRelation relation = relationItr.next();

if( (relation == null) || relation.getAsset().getPath().equalsIgnoreCase(relatedAssetPath)){
return;
}
}

graniteAsset.addRelation(name, relatedAssetPath);
}

private static String createAssetPath(Resource parent, String assetNameNoExt, String renditionName) {
return parent.getPath() + "/" + assetNameNoExt + "-" + renditionName;
}


private String getPayloadPath(WorkflowData wfData) {
String payloadPath = null;

if (wfData.getPayloadType().equals("JCR_PATH")) {
payloadPath = (String)wfData.getPayload();
}

return payloadPath;
}

}


2) Create a regular workflow model Experience AEM - Asset Create - Post Process and copy the Smart Tagging step and Workflow Completed steps from Assets Cloud Post Processing workflow...




3) Add the apps.experienceaem.assets.core.workfows.ConvertRenditionToAssetProcess step just before Workflow Completed step...


4) Create a Processing Profile to generate the rendition of Master asset...

                              https://author-p10961-e90064.adobeaemcloud.com/mnt/overlay/dam/gui/content/processingprofiles.html


5) Set the Processing Profile and Post processing workflow in parent folder properties...




AEM 6590 - Show Page References of Content Fragment in Metadata (Info) Page

$
0
0

Goal

References of a Content Fragment can be added in Metadata (Info) page by editing the content fragment metadata schema (http://localhost:4502/mnt/overlay/dam/gui/content/metadataschemaeditor/schemadetails.html/contentfragment?formPath=/conf/global/settings/dam/adminui-extension/metadataschema)

This post is on adding the References as an extension to show page name and path in separate columns...

Demo | Package Install | Github





Solution

Create a client library /apps/eaem-65-cf-references/clientlibs/cf-references with categories=dam.cfm.adminpage.v2 Add the following js logic in /apps/eaem-65-cf-references/clientlibs/cf-references/cf-references.js

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

var _ = window._,
initialized = false,
FRAGMENT_INFO_PAGE = "/mnt/overlay/dam/cfm/admin/content/v2/metadata-editor.html",
GET_REFERENCES_URL = "/libs/dam/content/schemaeditors/forms/references/items/tabs/items/tab1/items/col1/items/local-references/items/references.html";

if (!isFragmentInfoPage()) {
return;
}

init();

function init(){
if(initialized){
return;
}

initialized = true;

window.Dam.CFM.Core.registerReadyHandler(extendFragmentInfoPage);
}

function extendFragmentInfoPage(){
var cfPath = getCFPath();

$.ajax(GET_REFERENCES_URL + cfPath).done(function(html){
var $secondColumn = $("[data-metatype=tags]").closest(".aem-assets-metadata-form-column");

if($secondColumn.length === 0){

$secondColumn = $("[name='./jcr:content/metadata/cq:tags']").closest(".aem-assets-metadata-form-column");

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

addReferencesHtml($secondColumn, html);
})
}

function addReferencesHtml($secondColumn, html){
html = '<div class="coral-Form-fieldwrapper">' +
'<div class="coral-Form-fieldlabel" style="margin: 10px 0 5px 0">Page References</div>' + html +
'</div>';

$secondColumn.append(html);

$secondColumn.find("thead").remove();
}

function getCFPath(){
var path = window.location.pathname;

return path.substring(path.indexOf(FRAGMENT_INFO_PAGE) + FRAGMENT_INFO_PAGE.length);
}

function isFragmentInfoPage() {
return (window.location.pathname.indexOf(FRAGMENT_INFO_PAGE) >= 0);
}
}(jQuery, jQuery(document)));

AEM Cloud Service - Content Fragment RTE Plugin for Dynamic Variables

$
0
0

Goal

Adobe Experience Manager 2021.6.5586.20210628T210726Z-210600 (June 28, 2021)

Create a Content Fragment RTE Plugin (RichTextEditor) for Dynamic Variables. Variable is resolved with a value, when the CF is added on a page, fetched from Page Properties. As an example consider Credit Cards and Interest Rates. Interest Rate content is added in AEM as a Content Fragment and the actual interest rate is replaced with value when the CF is added on a specific Card page...

Demo | Package Install | CF Model | Github


Add Dynamic Variable


CF with Dynamic Variables


Dynamic Variable Unresolved


Dynamic Variable Value entered in Page Properties


Dynamic Variable Resolved with Value



Solution

1) Add the plugin /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/clientlib with categories=[dam.cfm.authoring.contenteditor.v2, eaem-cfm.rte.plugin] and dependencies=eaem.lodash. Add the plugin logic JS file /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/clientlib/dyn-var-plugin.js with following code...

(function ($, $document) {
var EAEM_PLUGIN_ID = "eaem-dyn-var",
EAEM_TEXT_DYN_VAR_FEATURE = "eaemDynVar",
EAEM_DYN_VAR_ICON = EAEM_PLUGIN_ID + "#" + EAEM_TEXT_DYN_VAR_FEATURE,
CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
DYN_VAR_SELECTOR_URL = "/apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/dyn-var-selector.html",
SENDER = "experience-aem", REQUESTER = "requester", $eaemDynVarPicker,
url = document.location.pathname;

if( (url.indexOf("/editor.html") == 0)
|| ( url.indexOf("/mnt/overlay/dam/cfm/admin/content/v2/fragment-editor.html") == 0) ){
extendStyledTextEditor();
registerPlugin();
}else if(url.indexOf(DYN_VAR_SELECTOR_URL) == 0){
handlePicker();
}

function handlePicker(){
$document.on("click", CANCEL_CSS, sendCancelMessage);

$document.submit(sendSelectedVars);
}

function sendSelectedVars(){
var message = {
sender: SENDER,
action: "submit",
data: {}
}, $form = $("form"), $field;

_.each($form.find("[name^='./']"), function(field){
if(!field.checked || (field.tagName !== "CORAL-CHECKBOX")){
return;
}

$field = $(field);
message.data[$field.attr("name").substr(2)] = $field.val();
});

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

function sendCancelMessage(){
var message = {
sender: SENDER,
action: "cancel"
};

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

function getParent() {
if (window.opener) {
return window.opener;
}

return parent;
}

function closePicker(event){
event = event.originalEvent || {};

if (_.isEmpty(event.data)) {
return;
}

var message, action;

try{
message = JSON.parse(event.data);
}catch(err){
return;
}

if (!message || message.sender !== SENDER) {
return;
}

action = message.action;

if(action === "submit"){
$eaemDynVarPicker.eaemFontPlugin.editorKernel.execCmd(EAEM_TEXT_DYN_VAR_FEATURE, message.data);
}

var modal = $eaemDynVarPicker.data('modal');
modal.hide();
modal.$element.remove();
}

function extendStyledTextEditor(){
var origFn = Dam.CFM.StyledTextEditor.prototype._start;

Dam.CFM.StyledTextEditor.prototype._start = function(){
addDynVarPluginSettings(this);
origFn.call(this);
}
}

function addDynVarPluginSettings(editor){
var config = editor.$editable.data("config");

config.rtePlugins[EAEM_PLUGIN_ID] = {
features: "*"
};

config.uiSettings.cui.multieditorFullscreen.toolbar.push(EAEM_DYN_VAR_ICON);
config.uiSettings.cui.inline.toolbar.push(EAEM_DYN_VAR_ICON);
}

function registerPlugin(){
var EAEM_CFM_DYN_VAR_PLUGIN = new Class({
toString: "eaemCFMDynVarPlugin",

extend: CUI.rte.plugins.Plugin,

textFontUI: null,

getFeatures: function () {
return [ EAEM_TEXT_DYN_VAR_FEATURE ];
},

notifyPluginConfig: function (pluginConfig) {
var defaults = {
tooltips: {}
};

defaults.tooltips[EAEM_TEXT_DYN_VAR_FEATURE] = {
title: "Select Dynamic Variable..."
};

CUI.rte.Utils.applyDefaults(pluginConfig, defaults);

this.config = pluginConfig;
},

initializeUI: function (tbGenerator) {
if (!this.isFeatureEnabled(EAEM_TEXT_DYN_VAR_FEATURE)) {
return;
}

this.textFontUI = new tbGenerator.createElement(EAEM_TEXT_DYN_VAR_FEATURE, this, false,
this.config.tooltips[EAEM_TEXT_DYN_VAR_FEATURE]);

tbGenerator.addElement(EAEM_TEXT_DYN_VAR_FEATURE, 999, this.textFontUI, 999);

if (tbGenerator.registerIcon) {
tbGenerator.registerIcon(EAEM_DYN_VAR_ICON, "brackets");
}

$(window).off('message', closePicker).on('message', closePicker);
},

isValidSelection: function(){
var winSel = window.getSelection();
return winSel && winSel.rangeCount == 1 && winSel.getRangeAt(0).toString().length > 0;
},

execute: function (pluginCommand, value, envOptions) {
if (pluginCommand != EAEM_TEXT_DYN_VAR_FEATURE) {
return;
}

this.showFontModal(this.getPickerIFrameUrl());
},

showFontModal: function(url){
var self = this, $iframe = $('<iframe>'),
$modal = $('<div>').addClass('eaem-cfm-font-size coral-Modal');

$iframe.attr('src', url).appendTo($modal);

$modal.appendTo('body').modal({
type: 'default',
buttons: [],
visible: true
});

$eaemDynVarPicker = $modal;

$eaemDynVarPicker.eaemFontPlugin = self;

$modal.nextAll(".coral-Modal-backdrop").addClass("cfm-coral2-backdrop");
},

getPickerIFrameUrl: function(){
return Granite.HTTP.externalize(DYN_VAR_SELECTOR_URL) + "?" + REQUESTER + "=" + SENDER;
}
});

var EAEM_CFM_DYN_VAR_CMD = new Class({
toString: "eaemDynVarCmd",

extend: CUI.rte.commands.Command,

isCommand: function (cmdStr) {
return (cmdStr.toLowerCase() == EAEM_TEXT_DYN_VAR_FEATURE);
},

getProcessingOptions: function () {
var cmd = CUI.rte.commands.Command;
return cmd.PO_SELECTION | cmd.PO_BOOKMARK | cmd.PO_NODELIST;
},

execute: function (execDef) {
execDef.value = Object.values(execDef.value).join("");

CUI.rte.commands.InsertHtml().execute(execDef);
},

queryState: function(selectionDef, cmd) {
return false;
}
});

CUI.rte.plugins.PluginRegistry.register(EAEM_PLUGIN_ID, EAEM_CFM_DYN_VAR_PLUGIN);

CUI.rte.commands.CommandRegistry.register(EAEM_TEXT_DYN_VAR_FEATURE, EAEM_CFM_DYN_VAR_CMD);
}
}(jQuery, jQuery(document)));


2) Create the plugin modal page /apps/eaem-cs-cf-rte-dyn-var/cfm-dyn-var-plugin/dyn-var-selector with Dynamic Variables configuration...

<?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="Dyn Variable Selector"
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, eaem-cfm.rte.plugin]"/>
</head>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/body">
<items jcr:primaryType="nt:unstructured">
<form
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form"
class="foundation-form content-container"
maximized="{Boolean}true"
style="vertical">
<items jcr:primaryType="nt:unstructured">
<wizard
jcr:primaryType="nt:unstructured"
jcr:title="Select the Dynamic Variable..."
sling:resourceType="granite/ui/components/coral/foundation/wizard">
<items jcr:primaryType="nt:unstructured">
<text
jcr:primaryType="nt:unstructured"
jcr:title="Select the Dynamic Variable..."
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<accordion
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/accordion"
margin="{Boolean}true"
variant="quiet">
<items jcr:primaryType="nt:unstructured">
<fees
jcr:primaryType="nt:unstructured"
jcr:title="Fees"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<membershipFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./membershipFee"
text="\{{membershipFee}}"
value="\{{membershipFee}}"/>
<balanceTransferMinFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./balanceTransferMinFee"
text="\{{balanceTransferMinFee}}"
value="\{{balanceTransferMinFee}}"/>
<cashAdvanceMinFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./cashAdvanceMinFee"
text="\{{cashAdvanceMinFee}}"
value="\{{cashAdvanceMinFee}}"/>
</items>
</fees>
</items>
</accordion>
</items>
<parentConfig jcr:primaryType="nt:unstructured">
<prev
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
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"
disabled="{Boolean}true"
text="Insert"
type="submit"
variant="primary">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="next"/>
</next>
</parentConfig>
</text>
</items>
</wizard>
</items>
</form>
</items>
</body>
</jcr:content>
</jcr:root>


3) Add a simple Static Template Component in project for testing purposes /apps/eaem-cs-cf-rte-dyn-var/components/basic-htl-page-component

<div  style="margin: 10px 25px 10px 25px">
<h2 style="text-align: center">Experience AEM CF Dynamic Variables Demo</h2>
<div data-sly-resource="${'content' @ resourceType='wcm/foundation/components/parsys'}"></div>
</div>


4) Add necessary properties for entering Dynamic Variable values in Page Dialog configuration /apps/eaem-cs-cf-rte-dyn-var/components/basic-htl-page-component/cq:dialog

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured">
<content jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<tabs jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<eaem
cq:showOnCreate="{Boolean}true"
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM"
sling:orderBefore="socialmedia"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<accordion
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/accordion"
margin="{Boolean}true"
variant="quiet">
<items jcr:primaryType="nt:unstructured">
<fees
jcr:primaryType="nt:unstructured"
jcr:title="Fees"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<membershipFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./dynVarmembershipFee"
fieldLabel="\{{membershipFee}}"/>
<balanceTransferMinFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./dynVarbalanceTransferMinFee"
fieldLabel="\{{balanceTransferMinFee}}"/>
<cashAdvanceMinFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./dynVarcashAdvanceMinFee"
fieldLabel="\{{cashAdvanceMinFee}}"/>
<foreignTransactionFee
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./dynVarforeignTransactionFee"
fieldLabel="\{{foreignTransactionFee}}"/>
</items>
</fees>
</items>
</accordion>
</items>
</column>
</items>
</eaem>
</items>
</tabs>
</items>
</content>
</jcr:root>


5) Add a Content Fragment Component for selecting the content fragment, resolving Dynamic Variables and rendering the html /apps/eaem-cs-cf-rte-dyn-var/components/dyn-vars-cf

<div style="width: 100%; border: 1px solid; padding: 20px"
data-sly-use.model="apps.experienceaem.assets.core.models.DynVarsCFModel"
data-sly-test="${model.modalData}">
<div style="color: red">
${model.modalData.eaemHeader}
</div>
<div style="margin-top: 10px">
${model.modalData.eaemContent @context='html'}
</div>
</div>
<div style="width: 100%; height: 30px; margin-top: 30px"
data-sly-use.model="apps.experienceaem.assets.core.models.DynVarsCFModel" data-sly-test="${!model.cfSelectedFrom && wcmmode.edit}">
Content Fragment not configured
</div>


6) Add a sling model apps.experienceaem.assets.core.models.DynVarsCFModel for resolving the Dynamic Variables with Values from Page Properties

package apps.experienceaem.assets.core.models;

import com.adobe.cq.dam.cfm.ContentElement;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.FragmentData;
import com.day.cq.wcm.api.Page;
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.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

@Inject
SlingHttpServletRequest request;

@Inject
Page currentPage;

@ValueMapValue
@Optional
private String fragmentPath;

@ValueMapValue
@Optional
private String cfSelectedFrom;

private String variation;
private Map<String,Object> modalData = new HashMap<String, Object>();

@PostConstruct
protected void init() {
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
ResourceResolver resolver = slingRequest.getResourceResolver();

Resource cfResource = null;
variation = slingRequest.getParameter("variation");

if(StringUtils.isEmpty(variation)){
variation = "master";
}

if("URL".equals(cfSelectedFrom)){
cfResource = slingRequest.getRequestPathInfo().getSuffixResource();
}else if(StringUtils.isNotEmpty(fragmentPath)){
cfResource = resolver.getResource(fragmentPath);
}

if(cfResource == null){
return;
}

modalData = getCFData(cfResource.adaptTo(ContentFragment.class), resolver, currentPage.getProperties());
}

private Map<String,Object> getCFData(ContentFragment cf, ResourceResolver resolver, ValueMap pageProps){
Map<String,Object> cfData = new HashMap<String, Object>();

Iterator<ContentElement> cfElementsItr = cf.getElements();

while(cfElementsItr.hasNext()){
ContentElement cfElement = cfElementsItr.next();

if(cfElement == null ){
continue;
}

Object fragValue = getVariationValue(cfElement, variation).getValue();

if(fragValue == null){
continue;
}else if(isMultiCF(cfElement)){
List<Object> multis = new ArrayList<Object>();

for(String linkPath : (String[])fragValue){
multis.add(getCFData(resolver.getResource(linkPath).adaptTo(ContentFragment.class), resolver, pageProps));
}

cfData.put(cfElement.getName(), multis);
}else{
cfData.put(cfElement.getName(), replaceDynVars(String.valueOf(fragValue), pageProps));
}
}

return cfData;
}

private String replaceDynVars(String fragValue, ValueMap pageProps){
Iterator<String> itr = pageProps.keySet().iterator();
String key;

while(itr.hasNext()){
key = itr.next();

if(!key.startsWith("dynVar")){
continue;
}

fragValue = fragValue.replace("{{" + key.substring("dynVar".length()) + "}}", String.valueOf(pageProps.get(key)));
}

return fragValue;
}

private boolean isMultiCF(ContentElement cfElement){
return cfElement.getValue().getDataType().isMultiValue();
}

public FragmentData getVariationValue(ContentElement cfElement, String variationName){
if(StringUtils.isEmpty(variationName) || "master".equals(variationName)){
return cfElement.getValue();
}

return cfElement.getVariation(variation).getValue();
}

public String getCfSelectedFrom() {
return cfSelectedFrom;
}

public Map<String,Object> getModalData(){
return modalData;
}
}


7) Add Design for configuring the Parsys component Allowed Components in /apps/settings/wcm/designs/experience-aem (In Cloud Services for static templates design always comes from configuration in /apps; there is no Design option like in AEM 65 or CS SDK to configure allowed components and stored in for eg. /libs/settings/wcm/designs/default/jcr:content/basic-htl-page-component)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Page">
<jcr:content
cq:lastModified="{Date}2017-07-20T15:17:11.670+01:00"
cq:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM Design"
sling:resourceType="wcm/core/components/designer">
<basic-htl-page-component jcr:primaryType="nt:unstructured">
<content
jcr:lastModified="{Date}2021-06-30T16:00:11.535-05:00"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
sling:resourceType="wcm/foundation/components/parsys"
components="[/apps/eaem-cs-cf-rte-dyn-var/components/dyn-vars-cf]">
<section jcr:primaryType="nt:unstructured"/>
</content>
</basic-htl-page-component>
</jcr:content>
</jcr:root>


8) Select the design /apps/settings/wcm/designs/experience-aem in Page Properties> Advanced


AEM Cloud Service - Content Fragments Usage in Pages Custom Asset Report

$
0
0

Goal

Create a Custom Asset Report Experience AEM Content Fragments Report for finding the usage of Content Fragments in a Site (or a section of site with root path)

Demo | Package Install | Github


Select Report Type


Report Configuration


Select Columns in Report


Reports Generated


Report View


Solution

1) Create the report type node /apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report

<?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"
description="Experience AEM Custom Reports Content Fragments Usage in Site Pages"
icon="data"
type="cf-usage-report"
title="Experience AEM Content Fragments Report"
wizard="/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report-wizard.html"/>


2) Create report wizard /apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report-wizard

<?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:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column1
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<field1
jcr:primaryType="nt:unstructured"
jcr:title="Title and Description"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<name
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="jobTitle"
required="{Boolean}true"/>
<description
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
fieldLabel="Description"
name="jobDescription"
rows="4"/>
</items>
</field1>
<field2
jcr:primaryType="nt:unstructured"
jcr:title="Content Fragments Root Path"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<pathbrowser
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
emptyText="/content/dam"
fieldLabel="Folder Path"
filter="folder"
name="cfRootPath"
predicate="folder"
rootPath="/content/dam"/>
</items>
</field2>
<field3
jcr:primaryType="nt:unstructured"
jcr:title="Pages Root Path"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<pathbrowser
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
emptyText="/content"
fieldLabel="Site Path"
name="pageRootPath"
predicate="folder"
rootPath="/content"/>
</items>
</field3>
</items>
</column1>
</items>
</jcr:root>


3) Add a filter apps.experienceaem.assets.core.filters.AddCustomReports to add the custom  CF report cf-usage-report to otb reports data source dam/gui/coral/components/commons/ui/shell/datasources/reportlistdatasource pulling from /libs/dam/content/reports/availablereports

package apps.experienceaem.assets.core.filters;

import com.adobe.granite.ui.components.Config;
import com.adobe.granite.ui.components.ExpressionResolver;
import com.adobe.granite.ui.components.PagingIterator;
import com.adobe.granite.ui.components.ds.AbstractDataSource;
import com.adobe.granite.ui.components.ds.DataSource;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.iterators.TransformIterator;
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.ResourceWrapper;
import org.apache.sling.api.resource.ValueMap;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Custom Reports - Add Custom Reports Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=INCLUDE",
"sling.filter.resourceTypes=dam/gui/coral/components/commons/ui/shell/datasources/reportlistdatasource"
}
)
public class AddCustomReports implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());

private static String ASSET_USAGE_REPORT = "/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report";

private static String OTB_REPORTS_PATH = "dam/content/reports/availablereports";

@Reference
private ExpressionResolver expressionResolver;

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

Resource resource = slingRequest.getResource();
Resource repConfigRes = resource.getChild(Config.DATASOURCE);

filterChain.doFilter(request, response);

if(repConfigRes == null){
return;
}

ValueMap repConfigVM = repConfigRes.getValueMap();
String reportPath = repConfigVM.get("reportPath", "");
String itemRT = repConfigVM.get("itemResourceType", "");

if(!OTB_REPORTS_PATH.equals(reportPath) || StringUtils.isEmpty(itemRT)){
return;
}

AbstractDataSource ds = (AbstractDataSource)request.getAttribute(DataSource.class.getName());

final List<Resource> sortedList = new ArrayList<Resource>();
Iterator<Resource> items = ds.iterator();

while(items.hasNext()){
sortedList.add(items.next());
}

sortedList.add(slingRequest.getResourceResolver().getResource(ASSET_USAGE_REPORT));

ds = new AbstractDataSource() {
public Iterator<Resource> iterator() {
return new TransformIterator(new PagingIterator(sortedList.iterator(), 0, 100), new Transformer() {
public Object transform(Object o) {
final Resource r = (Resource)o;
return new ResourceWrapper(r) {
public String getResourceType() {
return itemRT;
}
};
}
});
}
};

request.setAttribute(DataSource.class.getName(), ds);
}

@Override
public void init(FilterConfig filterConfig) {
}

@Override
public void destroy() {
}

}


4) Create a servlet apps.experienceaem.assets.core.servlets.EAEMReportGeneration for handling the requests from report creation wizard /mnt/overlay/dam/gui/content/reports/createreportwizard.html and create a sling job, put in the queue com/eaem/aem/dam/report for report generation...

package apps.experienceaem.assets.core.servlets;

import com.day.cq.commons.TidyJSONWriter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.JobManager;
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.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;

@Component(
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.selectors=eaemcfreport",
"sling.servlet.methods=POST",
"sling.servlet.resourceTypes=sling/servlet/default"
}
)
public class EAEMReportGeneration extends SlingAllMethodsServlet {
private final Logger logger = LoggerFactory.getLogger(getClass());

@Reference
private JobManager jobManager;

protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
ResourceResolver resourceResolver = request.getResourceResolver();

try{
String jobNodeName = UUID.randomUUID().toString();

HashMap<String, Object> jobProps = new HashMap<String, Object>();
jobProps.put("cfRootPath", request.getParameter("cfRootPath"));
jobProps.put("pageRootPath", request.getParameter("pageRootPath"));
jobProps.put("jobNodePath", "/var/dam/reports/" + jobNodeName);

List<String> columns = new ArrayList<String>();
columns.addAll(Arrays.asList(request.getParameterValues("column")));
columns.add("Page References");

jobProps.put("reportColumns", columns.toArray(new String[0]));

Node jobNode = createJobNode(resourceResolver, jobNodeName, request, columns);
Calendar createTime = Calendar.getInstance();
createTime.setTimeInMillis(createTime.getTimeInMillis());

Job job = jobManager.addJob("com/eaem/aem/dam/report", jobProps);

jobNode.setProperty("jobId", job.getId());
jobNode.setProperty("jobStatus", "processing");
jobNode.setProperty("jcr:created", createTime);

TidyJSONWriter writer = new TidyJSONWriter(response.getWriter());
writer.object();
writer.key("jobNodeName").value(jobNodeName);
writer.endObject();

resourceResolver.commit();
}catch(Exception e){
logger.error("Error scheduling export job", e);
}
}

private Node createJobNode(ResourceResolver resourceResolver, String jobNodeName,
SlingHttpServletRequest request, List<String> columns)
throws RepositoryException {
Session session = resourceResolver.adaptTo(Session.class);
String baseNodePath = "/var/dam/reports";
Node baseNode, jobNode;

if(resourceResolver.getResource(baseNodePath) == null) {
jobNode = session.getNode("/var/dam");
baseNode = jobNode.addNode("reports", "sling:Folder");
} else {
baseNode = session.getNode(baseNodePath);
}

jobNode = baseNode.addNode(jobNodeName.replaceAll("/", "-"), "nt:unstructured");
jobNode.setProperty("reportType", request.getParameter("dam-asset-report-type"));
jobNode.setProperty("cfRootPath", request.getParameter("cfRootPath"));
jobNode.setProperty("pageRootPath", request.getParameter("pageRootPath"));
jobNode.setProperty("jobTitle", request.getParameter("jobTitle"));
jobNode.setProperty("jobDescription", request.getParameter("jobDescription"));
jobNode.setProperty("reportColumns", columns.toArray(new String[0]));

session.save();

return jobNode;
}
}


5) Create a job consumer apps.experienceaem.assets.core.reports.EAEMReportJobConsumer, add the necessary search logic for finding the content fragments added on pages and create report (stored in /var/dam/reports)

package apps.experienceaem.assets.core.reports;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.LoginException;
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 javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.*;
import java.util.*;

@Component(
immediate = true,
service = JobConsumer.class,
property = {
"job.topics=com/eaem/aem/dam/report",
}
)
public class EAEMReportJobConsumer implements JobConsumer {
protected final Logger logger = LoggerFactory.getLogger(EAEMReportJobConsumer.class);

private static String CF_USAGE_REPORT_TYPE = "cf-usage-report";

@Reference
private ResourceResolverFactory resolverFactory;

@Reference
private QueryBuilder builder;

public JobConsumer.JobResult process(Job job) {
ResourceResolver resourceResolver = null;
File tempFile = null;

try {
resourceResolver = getServiceResourceResolver(resolverFactory);
Session session = resourceResolver.adaptTo(Session.class);

String cfRootPath = job.getProperty("cfRootPath", String.class);

if(StringUtils.isEmpty(cfRootPath)){
cfRootPath = "/content/dam";
}

String pageRootPath = job.getProperty("pageRootPath", String.class);

if(StringUtils.isEmpty(pageRootPath)){
pageRootPath = "/content";
}

String csvCreationPath = job.getProperty("jobNodePath", String.class);
Node jobNode = session.getNode(csvCreationPath);
String csvName = jobNode.getProperty("jobTitle").getString();
ArrayList columns = new ArrayList(Arrays.asList(job.getProperty("reportColumns", String[].class)));

String reportType = jobNode.getProperty("reportType").getString();

if(!reportType.equals(CF_USAGE_REPORT_TYPE)){
return JobResult.OK;
}

tempFile = File.createTempFile("report", ".csv");
FileOutputStream e = new FileOutputStream(tempFile);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(e, "UTF-8"));

List csvHeaders = writeColumnsHeaderToCSV(writer, columns);
writeContentToCSV(writer, resourceResolver, cfRootPath,pageRootPath);

writer.close();

Node fileNode = jobNode.addNode(csvName + ".csv", "nt:file");
Node resNode = fileNode.addNode("jcr:content", "nt:resource");

jobNode.setProperty("reportCsvColumns", (String[])csvHeaders.toArray(new String[0]));
resNode.setProperty("jcr:mimeType", "text/csv");
resNode.setProperty("jcr:data", session.getValueFactory().createBinary(
new ByteArrayInputStream(FileUtils.readFileToByteArray(tempFile))));

setLastModified(resNode);

jobNode.setProperty("jobStatus", "completed");

session.save();
} catch (Exception var28) {
logger.info("Failed to create report");
return JobResult.FAILED;
} finally {
if(tempFile != null) {
tempFile.delete();
}
}

return JobResult.OK;
}

public ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
Map<String, Object> subServiceUser = new HashMap<>();
subServiceUser.put(ResourceResolverFactory.SUBSERVICE, "eaem-user-report-admin");
try {
return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
} catch (LoginException ex) {
logger.error("Could not login as SubService user {}", "eaem-user-report-admin", ex);
return null;
}
}


public List<String> writeColumnsHeaderToCSV(PrintWriter writer, List<String> columns) throws IOException {
List<String> csvColumns = new ArrayList<String>();

columns.stream().forEach((c) -> {
writer.append("\"").append(c.toUpperCase()).append("\"").append(",");
csvColumns.add(c.toLowerCase());
});

writer.append("\r\n");

return csvColumns;
}

private static Map<String, String> getFindCFsQueryPredicateMap(String folderPath) {
Map<String, String> map = new HashMap<>();

map.put("path", folderPath);
map.put("1_property","jcr:content/contentFragment");
map.put("1_property.value","true");
map.put("p.limit","-1");

return map;
}

private static Map<String, String> getFindReferencesPredicateMap(String folderPath, String cfPath) {
Map<String, String> map = new HashMap<>();

map.put("path", folderPath);
map.put("fulltext", cfPath);
map.put("orderby", "@jcr:score");
map.put("p.limit", "-1");

return map;
}

private void setLastModified(Node resNode) throws RepositoryException {
Calendar lastModified = Calendar.getInstance();
lastModified.setTimeInMillis(lastModified.getTimeInMillis());
resNode.setProperty("jcr:lastModified", lastModified);
}

public void writeContentToCSV(PrintWriter writer, ResourceResolver resolver, String folderPath, String pageRootPath) throws Exception {
Query query = builder.createQuery(PredicateGroup.create(getFindCFsQueryPredicateMap(folderPath)), resolver.adaptTo(Session.class));

SearchResult result = query.getResult();
String cfPath = null, title;

for (Hit hit : result.getHits()) {
cfPath = hit.getPath();

Query cfQuery = builder.createQuery(PredicateGroup.create(getFindReferencesPredicateMap(pageRootPath, cfPath)), resolver.adaptTo(Session.class));
SearchResult cfResults = cfQuery.getResult();

for (Hit cfHit : cfResults.getHits()) {
writer.append("\"").append(hit.getTitle()).append("\"").append(",").append("\"")
.append(hit.getPath()).append("\"").append(",").append("\"").append(cfHit.getPath()).append("\",");
writer.append("\r\n");
}
}
}
}


6) Create a clientlib /apps/eaem-cs-asset-ref-report/clientlibs/report-list with categories=cq.dam.admin.reportlist and dependencies=eaem.lodash for adding the report type in list view...

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

var _ = window._,
initialized = false,
REPORT_TYPE_DETAIL = "Experience AEM Content Fragments Usage Report",
REPORT_LIST_PAGE = "/mnt/overlay/dam/gui/content/reports/reportlist.html";

if (!isReportListPage()) {
return;
}

init();

function init(){
if(initialized){
return;
}

initialized = true;

$(document).one("foundation-contentloaded", function(e){
$("[value='null']").html(REPORT_TYPE_DETAIL);
});
}

function isReportListPage() {
return (window.location.pathname.indexOf(REPORT_LIST_PAGE) >= 0);
}
}(jQuery, jQuery(document)));


7) Create a clientlib /apps/eaem-cs-asset-ref-report/clientlibs/report-wizard with categories=cq.dam.admin.createreportwizard and dependencies=eaem.lodash to change the form action when report type is cf-usage-report

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

var _ = window._,
initialized = false,
REPORT_TYPE = "cf-usage-report",
EXPORT_REQ_URL = "/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report.eaemcfreport.json",
REPORT_TYPE_DETAIL = "Experience AEM Content Fragments Usage Report",
REPORT_WIZARD = "/mnt/overlay/dam/gui/content/reports/createreportwizard.html";

if (!isReportWizard()) {
return;
}

init();

function init(){
if(initialized){
return;
}

initialized = true;

$(document).one("foundation-contentloaded.cq-damadmin-createreport-wizard", function(e){
$(".foundation-wizard", e.target).on("foundation-wizard-stepchange", setExportUrl);
});
}

function setExportUrl(e, to){
if( (getStepNumber() != 3) || (REPORT_TYPE !== getReportType())){
return;
}

var $lastStep = $(to);

$lastStep.on("foundation-contentloaded",function(){
$(".cq-dam-assetthumbnail").find("coral-card-title").html(REPORT_TYPE_DETAIL);
$("form").prop("action", EXPORT_REQ_URL);
});
}

function getStepNumber(){
var $wizard = $(".foundation-wizard"),
currentStep = $wizard.find(".foundation-wizard-step-active"),
wizardApi = $wizard.adaptTo("foundation-wizard");

return wizardApi.getPrevSteps(currentStep).length + 1;
}

function getReportType(){
var reportPath = $("input[name='dam-asset-report-type'] ").val();
return reportPath.substring(reportPath.lastIndexOf("/") + 1);
}

function isReportWizard() {
return (window.location.pathname.indexOf(REPORT_WIZARD) >= 0);
}
}(jQuery, jQuery(document)));


7) Create a sling repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-asset-ref-report\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config for adding a service user eaem-user-report-admin used in report generation logic

scripts=[
"
create service user eaem-user-report-admin with path system/cq:services/experience-aem
set principal ACL for eaem-user-report-admin
allow jcr:all on /var
allow jcr:read on /apps
allow jcr:read on /content
end
"
]


8) Add the necessary bundle to service user mapping script ui.config\src\main\content\jcr_root\apps\eaem-cs-asset-ref-report\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-eaem.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"
user.mapping="[eaem-cs-asset-ref-report.core:eaem-user-report-admin=[eaem-user-report-admin]]"/>


Viewing all 525 articles
Browse latest View live


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