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

AEM Cloud Service - Firefly Headless App with Asset Compute worker for creating High Res Renditions

$
0
0

In this post we'll create a Headless Firefly App and add a Asset Compute Worker for generating High Res JPEG renditions using the Photoshop I/O Apihttps://adobedocs.github.io/photoshop-api-docs/

Project Firefly (still in beta) is a framework for building Micro Services Apps that run in Experience Cloud Runtime, you can learn more about it here - https://github.com/AdobeDocs/project-firefly/blob/master/overview/what_is.md

Asset Compute documentation for creating a custom worker is available here




Create Adobe I/O Project

1) Assuming your org was provisioned for Adobe Experience Cloud (plus Creative Cloud) products and you are a System Administrator in Adobe Developer Consolehttps://console.adobe.io, login using your adobe id and create a new project...



2) Add a new workspace for your dev firefly app...



3) In your IO workspace add the Asset Compute Service. In this step, you'll generate a private/public key pair for adding in the app configuration later....



4) Add the I/O Events and I/O Management API Services....



5) Add the Photoshop API Service (assuming your org was provisioned for using Creative Cloud Services). This is required for submitting a Create Rendition job from your micro services worker...



6) With all the necessary services added, here is how your project should look...




Create FireFly App

7) Install latest NodeJS (> v10) https://nodejs.org/en/download/ check the version  node -v

8) Install AIO command line aio-cli from https://github.com/adobe/aio-cli#usage 

                         npm install -g @adobe/aio-cli 

check version is > 3.7.0 

                         npm show @adobe/aio-cli version

9) Create your app eaem-ps-high-res-rend-worker

                         aio app init --asset-compute eaem-ps-high-res-rend-worker

10) Follow the prompts and provide the app configuration...



11) Create the integration file ps-asset-compute-integration.yaml, add the private/public key, org specific values.... 



12) Provide the integration file path as value for parameter ASSET_COMPUTE_INTEGRATION_FILE_PATH in the app eaem-ps-high-res-rend-worker/.env file



13) Add your worker specific logic in eaem-ps-high-res-rend-worker\actions\eaem-ps-high-res-rend-worker\index.js generating a Azure SAS (Shared Access Signature) token, generate presigned url for uploading the asset to shared storage accessible to PS API for storing the generate renditions....

'use strict';

const { worker, SourceCorruptError, GenericError } = require('@adobe/asset-compute-sdk');
const { downloadFile } = require("@adobe/httptransfer");
const fetch = require("@adobe/node-fetch-retry");
const fs = require('fs').promises;
const util = require("util");
const sleep = util.promisify(setTimeout);
const AzureStorage = require('azure-storage');

class EAEMPhotoshopService {
constructor(auth) {
this.auth = auth;
}

async createRendition(sourceURL, renditionStoreUrl){
const URL = "https://image.adobe.io/pie/psdService/renditionCreate";

const body = JSON.stringify({
inputs: [{
storage: "azure",
href: sourceURL
}],
outputs: [{
storage: "azure",
href: renditionStoreUrl,
type: "image/jpeg",
"overwrite": true,
"quality": 7 // generate a high quality rendition
}]
});

const response = await fetch(URL, {
method: "post",
body,
headers: this._headers()
});

if (response.ok) {
const respJSON = await response.json();

console.log("EAEM Job Status Url: " , respJSON._links.self.href);

return respJSON._links.self.href;
}else{
console.log("EAEM error posting rendition request: " , response);
}

return null;
}

async checkStatus(statusUrl) {
const response = await fetch(statusUrl, {
headers: this._headers()
});

if (response.ok) {
return (await response.json());
} else {
console.log("EAEM: Error checking status", response);
}

return null;
}

_getAzureReadWritePresignedUrl(instructions) {
const blobService = AzureStorage.createBlobService(instructions["AZURE_STORAGE_ACCOUNT"],
instructions["AZURE_STORAGE_KEY"]);

const sharedAccessPolicy = {
AccessPolicy: {
Permissions: 'racwdl',
Start: new Date(),
Expiry: new Date(new Date().getTime() + 60 * 60000) // expire in 1 hour
}
};

const psRenditionPath = "photoshop/" + instructions.userData.path;

var token = blobService.generateSharedAccessSignature(instructions["AZURE_STORAGE_CONTAINER_NAME"], psRenditionPath,
sharedAccessPolicy);

return blobService.getUrl(instructions["AZURE_STORAGE_CONTAINER_NAME"], psRenditionPath, token);
}

_headers() {
return {
"Authorization": `Bearer ${this.auth.accessToken}`,
"x-api-key": this.auth.clientId,
"Content-Type": "application/json"
};
}
}

exports.main = worker(async (source, rendition, params, other) => {
const stats = await fs.stat(source.path);

if (stats.size === 0) {
throw new SourceCorruptError('EAEM: Source file is empty');
}

console.log("EAEM Rendition Instructions for the app...", rendition.instructions);

const SERVICE = new EAEMPhotoshopService(params.auth);
const psRendUrl = SERVICE._getAzureReadWritePresignedUrl(rendition.instructions);
const statusUrl = await SERVICE.createRendition(source.url, psRendUrl);

if(!statusUrl){
throw new GenericError("EAEM: Error submitting rendition request");
}

let retries = rendition.instructions["REND_GEN_STATUS_RETRIES"] || "5";

retries = parseInt(retries);

while (true) {
if(retries-- <= 0){
console.log("EAEM: Exhausted retries for rendition generation check, quitting now...");
break;
}

const result = await SERVICE.checkStatus(statusUrl);

if(!result || result.outputs[0].errors){
console.log("EAEM: Rendition generation failed...", result.outputs[0].errors);
throw new GenericError("EAEM: Error generating rendition");
}else if(result.outputs[0].status == "succeeded" ){
console.log("EAEM: Rendition generation successful, available at : " + rendition.target);
break;
}

await sleep(1000);
}

await downloadFile(psRendUrl, rendition.path);
});

14) Install Docker Desktop https://www.docker.com/get-started. No further configuration required, you need docker running to run the worker tests....

15) If you are on windows, open command prompt, followed by the Git Bash shell (the app needs some linux specific commands for execution)



14) Execute the default unit tests aio app test



15) Run the app aio app run. This will run a local dev server and open the a browser window http://localhost:9000/ to test your worker... the worker input is provided as a JSON

{
"renditions": [
{
"worker": "https://126827-eaemhighresrendition-sreekassetcomputeps.adobeioruntime.net/api/v1/web/EAEMHighResRendition-0.0.1/eaem-ps-high-res-rend-worker",
"name": "rendition.jpg",
"REND_GEN_STATUS_RETRIES": 15,
"AZURE_STORAGE_ACCOUNT": "ACCOUNT",
"AZURE_STORAGE_KEY": "KEY",
"AZURE_STORAGE_CONTAINER_NAME" : "sreek"
}
]
}



16)  When the dev server is running, to update the app logic use aio app deploy

17) Folders in azure cloud storage used by the app for intermediate storage....






AEM 6550 - ACS 19.3 - Selecting AEM Assets in Adobe Campaign Standard

$
0
0

Use AEM managed assets in emails created using Adobe Campaign Standard (ACS). The process discussed in this post uses Adobe Core Service for syncing assets to ACS, whereas the solution below is a bit more direct for selecting assets managed in AEM...



Email Creation in ACS

1) Follow the Create an Email process in Adobe Campaign Standard...



2) Select Email recipients...



3) Use the Email Designer for creating email...



4) Create a column layout, drag and drop Text components, add some content, followed by Html component....



5) Click on the source code menu item of Html Component...

6) Source editing of Html Component...




Open AEM Asset Selector

7) In a new browser tab, enter AEM Asset Selector URL http://localhost:4502/aem/assetpicker. Depending on the AEM SSO configuration,  you may have to login with credentials...



8) Only published AEM assets can be used in ACS emails... so when an Unpublished asset is selected the picker shows warning message....



9) Select a Published asset, click "Img Html" button and a modal should open with published asset urlhtml block (this is a UI extension and can be further customized...). Click "Copy"to copy the html to clipboard....




Add AEM Asset in ACS

10) "Paste"the copied Html block in ACS Html Component source editor...



11) AEM asset should now show up in email editor (since the asset is served from AEM publish instance, no authentication is required here....)



12) Enter Email Subject, Prepare & Send the email...



13) Email delivered to inbox (with image rendered from AEM Publish instance...)





Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-asset-selector-img-html

2) For creating asset Publish URL using Externalizer add script /apps/eaem-asset-selector-img-html/publish-url/publish-url.jsp with the following code....

<%@page import="com.day.cq.commons.Externalizer" %>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0" %>
<cq:defineObjects />
<%
Externalizer externalizer = resourceResolver.adaptTo(Externalizer.class);

String url = externalizer.externalLink(resourceResolver, Externalizer.PUBLISH, request.getScheme(),
slingRequest.getRequestPathInfo().getSuffix());

response.getWriter().print(url);
%>

3) Create node /apps/eaem-asset-selector-img-html/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.damadmin.assetselector], String[] property dependencies with value lodash.

4) Create file (nt:file) /apps/eaem-asset-selector-img-html/clientlib/js.txt, add

                        asset-selector-img-html.js

5) Create file (nt:file) /apps/eaem-asset-selector-img-html/clientlib/asset-selector-img-html.js, add the following code...

(function($, $document){
var PUBLISH_URL = "/apps/eaem-asset-selector-img-html/publish-url/content.html";

$document.ready(addImgHtmlButton);

function addImgHtmlButton(){
var $titleBar = $(".granite-pickerdialog-titlebar"),
$imgHtmlBut = $titleBar.find("betty-titlebar-primary").append(getButtonHtml());

$imgHtmlBut.on('click', showHTMLInModal);
}

function showHTMLInModal(event){
event.preventDefault();

var $selections = $(".foundation-selections-item");

if(_.isEmpty($selections)){
showAlert("Please select an image", "Select");
return;
}

var $selection = $($selections[0]),
imgPath = $selection.data("granite-collection-item-id");

$.ajax( { url: imgPath + ".2.json", async: false}).done(function(data){
if(!data || !data["jcr:content"] || !data["jcr:content"]["cq:lastReplicated"]){
showAlert("Please publish the image in AEM for using it in Campaign...", "Publish");
imgPath = undefined;
}
});

if(!imgPath){
return;
}

var html = "<textarea rows='3' cols= '80'" +
"style='background-color:#EEE; outline: 0; border-width: 0;padding: 20px; font-family: Courier'>&ltdiv&gt";

$.ajax( { url: PUBLISH_URL + imgPath, async: false}).done(function(url){
imgPath = url.trim();
});

html = html + "\n\t" + getImageHtml(imgPath) + "\n" + "&lt/div&gt</textarea>";

showCopyCode(html);
}

function getButtonHtml(){
return '<button is="coral-button" icon="adobeCampaign" iconsize="S" variant="quiet">Img Html</button>';
}

function getImageHtml(imgPath){
return "<img src='" + imgPath + "'/>";
}

function showCopyCode(code, callback){
var fui = $(window).adaptTo("foundation-ui"),
options = [{
id: "cancel",
text: "Cancel",
primary: true
},{
id: "COPY",
text: "Copy",
primary: true
}];

fui.prompt("Code", code, "default", options, copyHandler);

function copyHandler(button){
if(button === 'cancel'){
return;
}

var $dialog = $("coral-dialog.is-open"),
$codeText = $dialog.find("textarea");

$codeText[0].select();

document.execCommand("copy");
}
}

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 6550 - Sites - Set Pages default sort to name (or title) in Sites Console

$
0
0

Pages in Sites console - /sites.html are not alphabetically sorted by default (the order is, as shown in crxde). The following configuration is for sorting them alphabetically during load...

For Assets Console, check this post



Product



Extension



Configuration

                          Overlay /libs/wcm/core/content/sites/jcr:content/views/list/datasource into /apps and set the following properties... 

                          sortName=main
                          sortDir=desc (it should be asc, but i guess a bug in product)

                          sortName can be name | main | modified | published | template | <custom column>




DM Classic - S7 - Show default image when a specific Image or Vignette object not found

$
0
0

Missing Product Image

In Dynamic Media Classic (S7), if the image (specified in URL with others modifiers) is NOT found, you can serve a default image by adding parameter &defaultImage. This helps when there are new images for a product not published yet or missing for some reason, so when users clicks the link or the specific swatch, a default image for the product is shown (instead of a broken or generic 404 image...)











Unavailable Vignette Object

While serving an Image Render or Vignette (composed of objects) if the Object name is NOT found, there is no default image parameter here and error Object not found is shown...






Workaround below is for showing a default product image when specified object name (in url parameters) is unavailable...


             Objects Available:



             Object Not Found





1) For running this sample with code in a local file and image we are requesting existing on a different domain (http://sample.scene7.com/) with CORS headers not set in response... we need to tone down chrome security a bit...

                     i. Create a new profile in chrome...



                   ii. Create a shortcut with target : 

                                     "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --profile-directory="Profile 1"  
                                      --user-data-dir="C:/Users/<user>/AppData/Local/Google/Chrome/User Data/Profile 1" --disable-web-security 



                   iii.  Run the new shortcut to open a browser instance disabling CORS...


2) Add the following JS logic in your html to first request the vignette contents using ?req=contents and match the object names in image request url parameters for validation...

    (function () {
$(document).ready(function(){
$("#eaem-s7-run-code").click(showImages);

showImages();
});

function showImages() {
var imageUrl = $("#eaem-s7-vignette-url").val();

showImage(imageUrl);

checkContentsAndShowImage(imageUrl);
}

function showImage(imageUrl) {
$("#eaem-s7-default-image").attr("src", imageUrl);
}

function checkContentsAndShowImage(imageUrl) {
var contentsUrl = imageUrl.substring(0, imageUrl.indexOf("?")) + "?req=contents",
objectsInVignette = [];

$.ajax({type: "GET", url: contentsUrl, dataType: "xml", async: false}).done(function (xml) {
objectsInVignette = checkVignetteObjects(xml);
});

var objectParamsInUrl = getImageURLObjParameters(imageUrl),
invalidObjects = [];

for(var x = 0; x < objectParamsInUrl.length; x++){
if(!objectsInVignette.includes(objectParamsInUrl[x])){
imageUrl = imageUrl.substring(0, imageUrl.indexOf("?"));
invalidObjects.push(objectParamsInUrl[x]);
}
}

$("#eaem-s7-vignette-objects").html(objectsInVignette.join(" | "));

$("#eaem-s7-url-objects").html(objectParamsInUrl.join(" | "));

$("#eaem-s7-url-invalid-objects").html(invalidObjects.join(" | "));

$("#eaem-s7-ext-image").attr("src", imageUrl);
}

function checkVignetteObjects(xml) {
var path = "/vignette/contents/group",
nodes = xml.evaluate(path, xml, null, XPathResult.ANY_TYPE, null),
group = nodes.iterateNext(), objects = [];

while (group) {
var object = group.getAttribute("id");

objects.push(object);

var children = group.getElementsByTagName("object");

for (var x = 0; x < children.length; x++) {
objects.push(object + "/" + children[x].id);
}

group = nodes.iterateNext();
}

return objects;
}

function getImageURLObjParameters(imageUrl) {
var paramsStr = imageUrl.substring(imageUrl.indexOf("?") + 1),
objParams = [], index, objName;

while(true){
index = paramsStr.indexOf("obj=");

if(index == -1){
break;
}

paramsStr = decodeURIComponent(paramsStr.substring( index + "obj=".length));

objName = paramsStr.substring(0, paramsStr.indexOf("&"));

objParams.push(objName);
}

return objParams;
}
}())

AEM 6550 - Log AEM Form Login User (j_username) Password (j_password) doing j_security_check Authentication

$
0
0

If you are unsure the user authentication information like j_usernamej_password are passed to /libs/granite/core/content/login.html/j_security_check or being stripped off from the request by intermediate proxies before reaching AEM, you can use the following code to log them... 

FOR DEBUGGING PURPOSES ON DEV INSTANCES ONLY, NOT PRODUCTION






Solution


Add the following custom authentication handler in a separate bundle (independent of your project) or install this package with the auth handler and log configuration logging to http://localhost:4502/system/console/slinglog/tailer.txt?tail=10000&grep=*&name=%2Flogs%2Feaem-jsecurity.log

package apps.experienceaem.assets;

import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import static org.osgi.framework.Constants.SERVICE_RANKING;

@Component(
service = { AuthenticationHandler.class },
immediate = true,
property = {
SERVICE_RANKING + ":Integer=" + 9999,
AuthenticationHandler.PATH_PROPERTY + "=/libs/granite/core/content/login.html/j_security_check",
AuthenticationHandler.TYPE_PROPERTY + "=" + "EAEM_RECORD_CREDS",
"service.description=Experience AEM Log j_security_check User Password Credentials"
})
public class EAEMRecordUserPassAuthHandler implements AuthenticationHandler, AuthenticationFeedbackHandler {

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

private static final String REQUEST_METHOD = "POST";
private static final String REQUEST_URL_SUFFIX = "/j_security_check";

@Reference
private SlingRepository repository;

@Reference(target = "(service.pid=com.day.crx.security.token.impl.impl.TokenAuthenticationHandler)")
private AuthenticationHandler wrappedAuthHandler;

public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
if (REQUEST_METHOD.equals(request.getMethod()) && request.getRequestURI().endsWith(REQUEST_URL_SUFFIX)) {
AuthenticationInfo authInfo = wrappedAuthHandler.extractCredentials(request, response);

SimpleCredentials sc = (SimpleCredentials) authInfo.get("user.jcr.credentials");

log.debug("User: " + sc.getUserID() + ", Password : " + new String(sc.getPassword()));

return authInfo;
}

return null;
}

public boolean requestCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
return wrappedAuthHandler.requestCredentials(httpServletRequest, httpServletResponse);
}

public void dropCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
wrappedAuthHandler.dropCredentials(httpServletRequest, httpServletResponse);
}

@Override
public void authenticationFailed(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) {
if (wrappedAuthHandler instanceof AuthenticationFeedbackHandler) {
((AuthenticationFeedbackHandler) wrappedAuthHandler).authenticationFailed(request, response, authInfo);
}
}

@Override
public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) {
if (wrappedAuthHandler instanceof AuthenticationFeedbackHandler) {
return ((AuthenticationFeedbackHandler) wrappedAuthHandler).authenticationSucceeded(request, response, authInfo);
}
return false;
}
}

AEM 6550 - AEM Assets show Tags in List View

$
0
0

Show Tags in List View of Assets and Omni Search Consoles



Assets Console - List View

Omni Search - List View



Solution


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

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


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

<%@include file="/libs/granite/ui/global.jsp"%>
<%@ page import="org.apache.sling.api.resource.ValueMap" %>
<%@ page import="org.apache.sling.api.resource.Resource" %>
<%@ page import="com.day.cq.tagging.TagManager" %>
<%@ page import="com.day.cq.tagging.Tag" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="com.day.cq.dam.api.Asset" %>
<%@ page import="org.apache.commons.collections.CollectionUtils" %>
<%@ page import="org.apache.commons.lang3.ArrayUtils" %>
<%@taglib prefix="cq" uri="http://www.day.com/taglibs/cq/1.0"%>

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

Resource assetResource = resource;
TagManager tagManager = assetResource.getResourceResolver().adaptTo(TagManager.class);
String eaemTags = "";

if(assetResource.getResourceType().equals(ASSET_RES_TYPE)){
Object[] tags = (Object[])assetResource.adaptTo(Asset.class).getMetadata("cq:tags");

if(!ArrayUtils.isEmpty(tags)){
List<String> tagWords = new ArrayList<String>(tags.length);

Arrays.stream(tags).forEach(tag -> tagWords.add(tagManager.resolve(tag.toString()).getTitle()));

eaemTags = String.join(", ", tagWords);;
}
}
%>

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

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


AEM 6550 - Find the Asset Reference in S3 Data Store

$
0
0


For debugging purposes if you need the asset S3 unique id for locating it in Cloud Data Store...

1) Install the Package

2) Pass the asset path to script eg. http://localhost:4502/apps/eaem-get-s3-reference/content.html/content/dam/sreek-local/sreek_KBYG_Reservations_Video_V10.mov



3) Using AWS Tools (Cmdlets) for Windows PowerShell locate the blob (stored in flat structure....)


Set-AWSCredential -AccessKey MY_S3_ACCESS_KEY -SecretKey MY_LONG_S3_SECRET_KEY -StoreAs experience-aem-profile

Set-AWSCredential -ProfileName experience-aem-profile

Get-S3Bucket -BucketName experience-aem-bucket

Get-S3Object -BucketName experience-aem-bucket -Key c086d8a58a2787f99ef6dffb6fab8c1f2f501604bac16c1eff4c767cd77294fd



4) The script in /apps/eaem-get-s3-reference/eaem-get-s3-reference.jsp to get the S3 reference...

<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="com.day.cq.dam.commons.util.DamUtil" %>
<%@ page import="org.apache.sling.api.resource.Resource" %>
<%@ page import="com.day.cq.commons.jcr.JcrConstants" %>
<%@ page import="javax.jcr.Node" %>
<%@ page import="javax.jcr.Property" %>
<%@ page import="org.apache.jackrabbit.api.ReferenceBinary" %>
<%@ page import="org.apache.sling.api.resource.ResourceResolver" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%@ page import="com.day.cq.dam.api.Asset" %>

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

<%

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

if(StringUtils.isEmpty(assetPath)){
response.getWriter().print("No suffix provided, sample usage - /apps/eaem-get-s3-reference/content.html/content/eaem/big-video.mov");
return;
}

ResourceResolver eaemResolver = eaemSlingRequest.getResourceResolver();
Resource s3Resource = eaemResolver.getResource(assetPath);

String assetIdSha246 = getS3AssetIdFromReference(s3Resource);

response.getWriter().print("Asset : " + assetPath + "<BR><BR>");
response.getWriter().print("S3 Reference : " + assetIdSha246);
%>

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

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

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

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

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

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

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

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

s3AssetId = value.getReference();

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

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

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

return s3AssetId;
}
%>


AEM - Silly Developer Mistakes

$
0
0

 

1) When you see the property value modified or added is not reflecting in application, make sure you are editing the correct config file, eg. editing the s3 datastore config... there could be a possibility the file exists in install location author\crx-quickstart\install\org.apache.jackrabbit.oak.plugins.blob.datastore.S3DataStore.config, however the config values are picked from launchpad config author\crx-quickstart\launchpad\config\org\apache\jackrabbit\oak\plugins\blob\datastore\S3DataStore.config


2) When you see the property values not picked by application specified in the correct properties file, try to change the type for  eg. in author\crx-quickstart\launchpad\config\org\apache\jackrabbit\oak\plugins\blob\datastore\S3DataStore.config specifying the value as Integer was not being picked up..

                     presignedHttpDownloadURIExpirySeconds=I"86404"

However changing it to string worked (removed I from value...)

                     presignedHttpDownloadURIExpirySeconds="86404"


AEM 6550 - Extend AEM Asset Picker to select Image Smart Crops and Video Dynamic Renditions

$
0
0


AEM Asset Picker (http://localhost:4502/aem/assetpicker) out of the box does not show Dynamic Renditions for selecting them in third party applications (using iframe). This post is on extending the Asset Picker to show (and select) Video Renditions (encodes) and Image Smart Crops...

Demo | Package Install | Github


Product - Asset Details Video Renditions



Extension - Asset Picker Video Renditions (Published)



Extension - Asset Picker Message to Parent Window on Video Rendition select



Product - Asset Details Image Smart Crop Renditions



Extension - Asset Picker Smart Crops Renditions



Extension - Asset Picker Select Smart Crops Rendition




Solution

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-asset-selector-show-dyn-renditions

2) Create the following nt:file /apps/eaem-asset-selector-show-dyn-renditions/smart-crop-renditions/smart-crop-renditions.jsp to return the smart crop renditions of an image as JSON


<%@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" %>

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

response.setContentType("application/json");

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

dynVM = ((ValueMapResource)items.next()).getValueMap();

String name = String.valueOf(dynVM.get("breakpoint-name"));

dynRendition.put("name", name);
dynRendition.put("image", dynVM.get("copyurl"));
dynRendition.put("url", dynVM.get("copyurl"));

dynRenditions.put(name, dynRendition);
}

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


3) Set the datasource for Image Smart Crops /apps/eaem-asset-selector-show-dyn-renditions/smart-crop-renditions/renditions/datasource@sling:resourceTypedam/gui/components/s7dam/smartcrop/datasource


4) Create the following nt:file /apps/eaem-asset-selector-show-dyn-renditions/video-dyn-renditions/video-dyn-renditions.jsp to return the encodes of a video as JSON


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

<%@page session="false"
import="org.apache.sling.commons.json.JSONObject"%>
<%@ 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" %>

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

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

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

JSONObject dynRenditions = new JSONObject();

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

DynamicMediaRenditionProvider dmRendProvider = sling.getService(DynamicMediaRenditionProvider.class);
String s7Domain = String.valueOf(asset.getMetadata("dam:scene7Domain"));
String scene7FileAvs = String.valueOf(asset.getMetadata("dam:scene7FileAvs"));

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

List<Rendition> dmRenditions = dmRendProvider.getRenditions(asset, rules);
JSONObject dynRendition = new JSONObject();
String image = null, avsName = scene7FileAvs.substring(scene7FileAvs.lastIndexOf("/") + 1);

dynRendition.put("url", getScene7Url(s7Domain, scene7FileAvs));
dynRendition.put("image", getRendThumbnail(s7Domain, scene7FileAvs));
dynRendition.put("name", avsName);

dynRenditions.put(avsName, dynRendition);

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

image = dmRendition.getPath();

if(image.endsWith(".mp4")){
image = image.substring(0, image.lastIndexOf(".mp4"));
}

dynRendition.put("name", dmRendition.getName());
dynRendition.put("image", getRendThumbnail(s7Domain, image));
dynRendition.put("url", getScene7Url(s7Domain, dmRendition.getPath()));

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 getRendThumbnail(String s7Domain, String rendPath){
return s7Domain + "/is/image/" + rendPath + "?fit=constrain,1&wid=200&hei=200";
}
%>


5) Create node /apps/eaem-asset-selector-show-dyn-renditions/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.damadmin.assetselector], String[] property dependencies with value lodash.


6) Create file (nt:file) /apps/eaem-asset-selector-show-dyn-renditions/clientlib/js.txt, add

                        col-view-show-dyn-renditions.js

7) Create file (nt:file) /apps/eaem-asset-selector-show-dyn-renditions/clientlib/col-view-show-dyn-renditions.js, add the following code

(function($, $document){
var CORAL_COLUMNVIEW_PREVIEW = "coral-columnview-preview",
THUMB_PATH = "/_jcr_content/renditions/cq5dam.thumbnail.48.48.png",
EAEM_DATA_ASSET_PATH = "data-eaem-asset-path",
EAEM_RENDITION_DATA = "data-eaem-rendition",
EAEM_RENDITION_FIELD = "eaem-rendition-name",
EAEM_DONE_ACTION = "EAEM_DONE",
GET_SMART_CROPS_URL = "/apps/eaem-asset-selector-show-dyn-renditions/smart-crop-renditions/renditions.html",
GET_VIDEO_RENDS_URL = "/apps/eaem-asset-selector-show-dyn-renditions/video-dyn-renditions/renditions.html",
added = false, dynRendsCol;

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

$document.on("foundation-selections-change", function(){
var isSelected = handleSelections();

if(isSelected){
return;
}

getUIWidget(CORAL_COLUMNVIEW_PREVIEW).then(showDynamicRenditions);
});

function registerSelectListener(){
var saveHandler = getSaveHandler();

$document.off('click', '.asset-picker-done');

$(document).on("click", ".asset-picker-done", function(e) {
e.stopImmediatePropagation();
exportAssetInfo(e);
});
}

function exportAssetInfo(e){
var message = {
config: {
action: EAEM_DONE_ACTION
},
data: []
};

var $selItem = $("coral-columnview-item.is-selected"),
selected = JSON.parse($selItem.attr(EAEM_RENDITION_DATA));

message.data.push(selected);

console.log(message);

getParent().postMessage(JSON.stringify(message), $(".assetselector-content-container").data("targetorigin"));
}

function handleSelections(){
var $selItem = $("coral-columnview-item.is-selected");

if(_.isEmpty($selItem) || !$selItem[0].hasAttribute(EAEM_DATA_ASSET_PATH)){
return false;
}

var metaHtml = getMetaHtml($selItem.attr(EAEM_DATA_ASSET_PATH));

$selItem.prepend(metaHtml);

$(".asset-picker-done")[0].disabled = false;

return true;
}

function resetDynamicRenditionsColumnView(){
$("coral-columnview-column").on("coral-columnview-column:_activeitemchanged", function(){
added = false;

if(dynRendsCol){
$(dynRendsCol).remove();
}
});
}

function createDynamicRenditionsColumn($colPreview){
dynRendsCol = new Coral.ColumnView.Column().set({});

dynRendsCol._loadItems = function(count, item){};

var $titleValue = $colPreview.find("coral-columnview-preview-label:contains('Title')").next(),
$rendition = $("<coral-columnview-preview-label>Rendition</coral-columnview-preview-label>")
.insertAfter( $titleValue );

$("<coral-columnview-preview-value id='" + EAEM_RENDITION_FIELD + "'>Original</coral-columnview-preview-value>").insertAfter($rendition);
}

function isImage(typeValue){
if(!typeValue){
return false;
}

return (typeValue.trim() == "IMAGE");
}

function isVideo(typeValue){
if(!typeValue){
return false;
}
return (typeValue.trim() == "MULTIMEDIA");
}

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

if(added){
return;
}

added = true;

resetDynamicRenditionsColumnView();

var assetPath = $colPreview.attr("data-foundation-layout-columnview-columnid"),
$type = $colPreview.find("coral-columnview-preview-label:contains('Type')"),
typeValue = $type.next("coral-columnview-preview-value").html(),
thumbPath = assetPath + THUMB_PATH;

if(!isImage(typeValue) && !isVideo(typeValue)){
return;
}

createDynamicRenditionsColumn($colPreview);

var $dynRendsCol = $(dynRendsCol).insertBefore($colPreview),
$dynRendsColContent = $dynRendsCol.find("coral-columnview-column-content");

addOriginalImage();

var rendsUrl = isImage(typeValue) ? GET_SMART_CROPS_URL : GET_VIDEO_RENDS_URL;

rendsUrl = rendsUrl + assetPath;

_.defer(function(){
$.ajax( { url: rendsUrl, async: false } ).done(addDynRenditions);
});

function addDynRenditions(data){
var $dynRendColItem;

_.each(data, function(dynRendition, dynName){
$dynRendColItem = $(getDynamicRenditionsHtml(thumbPath, dynRendition, assetPath))
.appendTo($dynRendsColContent);

$dynRendColItem.click(showDynRendImage);
});

$dynRendsColContent.find("coral-columnview-item:first").click();
}

function addOriginalImage(){
var origImgSrc = $colPreview.find("img").attr("src"),
data = { image : origImgSrc, name : "Original" };

var $orig = $(getDynamicRenditionsHtml(thumbPath, data, assetPath)).appendTo($dynRendsColContent);

$orig.click(showDynRendImage);
}

function showDynRendImage(){
$colPreview.find("img").attr("src", $(this).attr("data-foundation-collection-item-id"));
$colPreview.find("#" + EAEM_RENDITION_FIELD).html($(this).find(".foundation-collection-item-title").html());
}
}

function getMetaHtml(assetPath){
var $meta = $('[data-foundation-collection-item-id="' + assetPath + '"]'),
metaHtml = "";

if(_.isEmpty($meta)){
return metaHtml;
}

$meta = $meta.find(".foundation-collection-assets-meta");

if(_.isEmpty($meta)){
return metaHtml;
}

return $meta[0].outerHTML;
}

function getDynamicRenditionsHtml(thumbPath, dynRendition, assetPath) {
return '<coral-columnview-item data-foundation-collection-item-id="' + dynRendition.image + '"' + EAEM_DATA_ASSET_PATH + '="' + assetPath
+'"' + EAEM_RENDITION_DATA + '="' + JSON.stringify(dynRendition).replace(/\"/g, """) + '">' +
'<coral-columnview-item-thumbnail>' +
'<img src="' + thumbPath + '" style="vertical-align: middle; width: auto; height: auto; max-width: 3rem; max-height: 3rem;">' +
'</coral-columnview-item-thumbnail>' +
'<div class="foundation-collection-item-title">' + dynRendition.name + '</div>' +
'</coral-columnview-item>';
}

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

function getSaveHandler(){
var handlers = $._data(document, "events")["click"];

return _.reject(handlers, function(handler){
return (handler.selector != ".asset-picker-done" );
})[0].handler;
}

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

var deferred = $.Deferred();

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

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

clearInterval(INTERVAL);

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

return deferred.promise();
}
}(jQuery, jQuery(document)));

AEM 6550 - Dynamic Media (Scene7) Allowed IP addresses Test Publish Context

$
0
0


Published Dynamic Media assets are publicly available. If the usecase is to restrict asset access to specific IP addresses, the scene7 account can be configured to use a test server so the unpublished assets can be viewed from specific IP addresses without authentication check product documentation


Allowed IPs Configuration

                                                     Setup> Application Setup> Publish Setup> Image Server


Test Server settings


Forbidden (View unpublished not allowed)


                                                     https://s7test1.scene7.com/is/image/EAEM/texas?$EAEM$


Allowed (View unpublished allowed from configured IPs)

                                                     https://s7test1.scene7.com/is/image/EAEM/texas?$EAEM$


Image Not Published (Not available on delivery servers yet)

                                                     https://s7d1.scene7.com/is/image/EAEM/texas?$EAEM$



Image Published (Available on delivery servers, open to everyone)

                                                     https://s7d1.scene7.com/is/image/EAEM/texas?$EAEM$


AEM 6550 - Modify the Filename on Upload Before Executing DAM Update Asset Workflow

$
0
0

 

Adjust the filename on upload (organization standards?), before executing DAM Update Asset workflow. The sample in this post adds a filter executing prior to com.day.cq.dam.core.impl.servlet.CreateAssetServlet, intercepting the file upload stream 

Code in this post was not production harnessed, make sure you test (and improve it) for large files, small files, binaries, text files etc..

For validating file names on client side check this post


Demo | Package Install | Github




Add a filter eg. apps.experienceaem.assets.EAEMChangeFileNameFilter intercepting the file name stream and adjust the name (here it replaces spaces with hyphens)


package apps.experienceaem.assets;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;
import org.osgi.framework.Constants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.Part;

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

@Component(
immediate = true,
service = Filter.class,
name = "Experience AEM Request Name Change Filter for CreateAssetServlet Streaming Requests",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=REQUEST"
})
public class EAEMChangeFileNameFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(EAEMChangeFileNameFilter.class);

private static final String STRING_MATCH = "";

public void init(FilterConfig filterConfig) throws ServletException {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof SlingHttpServletRequest) || !(response instanceof SlingHttpServletResponse)) {
chain.doFilter(request, response);
return;
}

final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;

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

Iterator parts = (Iterator)request.getAttribute("request-parts-iterator");

if( (parts == null) || !parts.hasNext()){
chain.doFilter(request, response);
return;
}

List<Part> otherParts = new ArrayList<Part>();
Part part = null;

while(parts.hasNext()) {
part = (Part) parts.next();

otherParts.add(new EAEMFileNameRequestPart(part));
}

request.setAttribute("request-parts-iterator", otherParts.iterator());

chain.doFilter(request, response);
}

private boolean isCreateAssetRequest(SlingHttpServletRequest slingRequest){
String[] selectors = slingRequest.getRequestPathInfo().getSelectors();

if(ArrayUtils.isEmpty(selectors) || (selectors.length > 1)){
return false;
}

return selectors[0].equals("createasset");
}

public void destroy() {
}

//code copied form https://svn.apache.org/repos/asf/sling/trunk/bundles/engine/src/main/java/org/apache/sling/engine/impl/parameters/RequestPartsIterator.java
private static class EAEMFileNameRequestPart implements Part {
private final Part part;
private final InputStream inputStream;

public EAEMFileNameRequestPart(Part part) throws IOException {
this.part = part;

if(!isFileNamePart(part)){
this.inputStream = new ByteArrayInputStream(IOUtils.toByteArray(part.getInputStream()));
}else{
this.inputStream = this.getFileNameAdjustedStream(part);
}
}

private InputStream getFileNameAdjustedStream(Part part) throws IOException{
String fileName = null;

try{
fileName = IOUtils.toString(part.getInputStream(), "UTF-8");
}catch(Exception e){
log.error("Error reading filename from stream...");
}

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

if(!fileName.contains(STRING_MATCH)){
log.debug("Return unprocessed file name : " + fileName);
return new ByteArrayInputStream(fileName.getBytes());
}

fileName = fileName.trim().replaceAll(STRING_MATCH, "-");

log.debug("Uploaded file name changed to : " + fileName);

return new ByteArrayInputStream(fileName.getBytes());
}

private boolean isFileNamePart(Part part){
return ("fileName".equals(part.getName()));
}

public InputStream getInputStream() throws IOException {
return inputStream;
}

public String getContentType() {
return part.getContentType();
}

public String getName() {
return part.getName();
}

public long getSize() {
return 0;
}

public void write(String s) throws IOException {
throw new UnsupportedOperationException("Writing parts directly to disk is not supported by this implementation, use getInputStream instead");
}

public void delete() throws IOException {
}

public String getHeader(String headerName) {
return part.getHeader(headerName);
}

public Collection<String> getHeaders(String headerName) {
return part.getHeaders(headerName);
}

public Collection<String> getHeaderNames() {
return part.getHeaderNames();
}

public String getSubmittedFileName() {
return part.getSubmittedFileName();
}

private <T> Collection<T> toCollection(Iterator<T> i) {
if ( i == null ) {
return Collections.emptyList();
} else {
List<T> c = new ArrayList<T>();
while(i.hasNext()) {
c.add(i.next());
}
return c;
}
}
}
}

AEM 6550 - Move Render Condition for Assets with References

$
0
0

 

A sample render condition to show Move action bar button in Asset console only if one (or some) of the assets in a folder have references (say they are part of a Dynamic Media Image Set)


Package Install | Github


Assets have references (Move shown)



None of Assets have references (Move not shown)


Solution

1) Overlay /libs/dam/gui/content/assets/jcr:content/actions/selection/moveasset/granite:rendercondition into /apps/dam/gui/content/assets/jcr:content/actions/selection/moveasset/granite:rendercondition 


2) Set /apps/dam/gui/content/assets/jcr:content/actions/selection/moveasset/granite:rendercondition@sling:resourceType to /apps/eaem-move-references-rendercondition



3) Create the Render condition /apps/eaem-move-references-rendercondition/eaem-move-references-rendercondition.jsp with following code 


<%@page session="false"
import="com.adobe.granite.ui.components.ComponentHelper,
com.adobe.granite.ui.components.Config,
org.apache.sling.api.resource.Resource,
org.apache.sling.api.resource.ValueMap,
com.adobe.granite.ui.components.rendercondition.RenderCondition,
com.adobe.granite.ui.components.rendercondition.SimpleRenderCondition,
javax.jcr.Node,
com.day.cq.dam.commons.util.UIHelper,
com.day.cq.dam.api.DamConstants" %>
<%@ page import="javax.jcr.Session" %>
<%@ page import="javax.jcr.query.QueryManager" %>
<%@ page import="javax.jcr.query.Query" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="javax.jcr.NodeIterator" %>
<%@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/>

<%

ComponentHelper cmp = new ComponentHelper(pageContext);
Config cfg = cmp.getConfig();
String path = cmp.getExpressionHelper().getString(cfg.get("path", String.class));
Resource contentRes = null;

if (path != null) {
contentRes = slingRequest.getResourceResolver().getResource(path);
} else {
contentRes = UIHelper.getCurrentSuffixResource(slingRequest);
}

if (contentRes == null) {
return;
}

Iterator<Resource> itr = contentRes.listChildren();
StringBuilder strBuilder = new StringBuilder();

strBuilder.append("//element(*, nt:unstructured)[");

while(itr.hasNext()){
strBuilder.append("@sling:resource = '").append(itr.next().getPath()).append("' or");
}

String queryStmt = strBuilder.toString();

queryStmt = queryStmt.substring(0, queryStmt.lastIndexOf("or")) + "]";

Session session = resourceResolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();

Query query = qm.createQuery(queryStmt, Query.XPATH);

NodeIterator results = query.execute().getNodes();

boolean showMove = results.hasNext();

if(showMove){
%>
<sling:include path="/libs/dam/gui/coral/components/commons/renderconditions/mainasset"/>
<%
}else{
request.setAttribute(RenderCondition.class.getName(), new SimpleRenderCondition(false));
}
%>

AEM 6550 - AEM Dynamic Media SPA React Container for Video Backgrounds

$
0
0


The following steps explain creating a React SPA Container component (eaem-sites-spa-dm-video-container/components/container) extending Core Container component (core/wcm/components/container/v1/container) for Video Backgrounds and Component (eg. text) overlays...


Demo | Package Install | Github

         


Dynamic Media Configuration


Sync Configuration at Folder Level


Set Dynamic Media Video Profile



Component Dialog


View as Published




Solution


1) Create the component /apps/eaem-sites-spa-dm-video-container/components/container extending Core Container component core/wcm/components/container/v1/container  (core container component does not provide render script for React) for the SPA Editor. In the next step we'd be creating a react render script (coded in ES6)...




2) Add the component render script in eaem-sites-spa-dm-video-container\ui.frontend\src\components\DMVideoContainer\DMVideoContainer.js with the following code...
import React from 'react';
import {MapTo, withComponentMappingContext, Container, ResponsiveGrid, ComponentMapping} from '@adobe/cq-react-editable-components';
import {Helmet} from "react-helmet";

class EAEMContainer extends Container {
get containerProps() {
let containerProps = super.containerProps;

containerProps.style = {
"width": '100%',
"height": '500px'
};

return containerProps;
}

get videoDivProps() {
return {
"id": "eaem-dm-video-viewer",
"style": {
"zIndex": "0",
"position": "relative"
}
};
}

get overlayDivProps() {
return {
"style" : {
...{
"position": "absolute",
"zIndex": "1"
} ,
...this.props.overlayDivStyle
}
};
}

componentDidMount() {
const timer = setInterval((() => {
if(window.s7viewers){
clearInterval(timer);
this.loadVideo()
}
}).bind(this), 500);
}

loadVideo(){
new window.s7viewers.VideoViewer({
"containerId": "eaem-dm-video-viewer",
"params": {
"asset": this.props.dmVideoEncode,
"serverurl": this.props.dmServerUrl,
"videoserverurl": this.props.dmVideoServerUrl
}
}).init();
}

render() {
return (
<div {...this.containerProps}>
{ this.props.dmVideoPath &&
<Helmet>
<script src={ this.props.dmVideoViewerPath }></script>
</Helmet>
}

{ this.props.dmVideoPath &&
<div {...this.videoDivProps}>
<div {...this.overlayDivProps}>
{ this.childComponents }
{ this.placeholderComponent }
</div>
</div>
}

{ !this.props.dmVideoPath &&
<div>
{ this.childComponents }
{ this.placeholderComponent }
</div>
}
</div>
);
}
}

export default MapTo('eaem-sites-spa-dm-video-container/components/container')(EAEMContainer);

3) DMVideoContainer was imported in ui.frontend\src\components\import-components.js

                             import './DMVideoContainer/DMVideoContainer';
                             import './Page/Page';
                             import './Text/Text';


4) #88 specifies the script was mapped to component eaem-sites-spa-dm-video-container/components/container for rendering..

                             export default MapTo('eaem-sites-spa-dm-video-container/components/container')(EAEMContainer);


5) #63 adds the Video Viewer JS (eg. https://s7d1.scene7.com/s7viewers/html5/js/VideoViewer.js) to the <head> tag...

                             <Helmet>
                                    <script src={ this.props.dmVideoViewerPath }></script>
                             </Helmet>


6) #48loadVideo() loads the video into container div eaem-dm-video-viewer

7) Create a Sling Model Exporter com.eaem.core.models.impl.EAEMDMVideoContainerModelImpl for exporting the component properties





8) Add the following code in com.eaem.core.models.impl.EAEMDMVideoContainerModelImpl.

package com.eaem.core.models.impl;

import com.adobe.cq.export.json.ContainerExporter;
import com.day.cq.dam.api.Asset;
import com.day.cq.wcm.foundation.model.responsivegrid.ResponsiveGrid;
import com.eaem.core.models.EAEMDMVideoContainerModelExporter;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import com.adobe.cq.export.json.ComponentExporter;

import javax.annotation.PostConstruct;
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.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import java.util.HashMap;
import java.util.Map;

@Model(
adaptables = {SlingHttpServletRequest.class},
adapters = {ContainerExporter.class, ComponentExporter.class},
resourceType = {"eaem-sites-spa-dm-video-container/components/container"}
)
@Exporter(
name = "jackson",
extensions = {"json"}
)
@JsonSerialize(as = EAEMDMVideoContainerModelExporter.class)
public class EAEMDMVideoContainerModelImpl extends ResponsiveGrid implements EAEMDMVideoContainerModelExporter{
@ScriptVariable
private Resource resource;

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
private String eaemDMVideo;

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
private String eaemDMEncode;

private Map<String, Object> metadata;

@PostConstruct
protected void initModel() {
super.initModel();

if( (this.resource == null) || StringUtils.isEmpty(eaemDMVideo)) {
return;
}

ResourceResolver resolver = this.resource.getResourceResolver();
Resource videoRes = resolver.getResource(eaemDMVideo);

if(videoRes == null){
return;
}

metadata = videoRes.adaptTo(Asset.class).getMetadata();
}

public String getDmAccountName(){
if(metadata == null){
return "";
}

String fileName = String.valueOf(metadata.get("dam:scene7File"));

if(StringUtils.isEmpty(fileName)){
return "";
}

return fileName.substring(0, fileName.indexOf("/"));
}

public String getDmServerUrl() {
if(metadata == null){
return "";
}

return metadata.get("dam:scene7Domain") + "is/image/";
}

public String getDmVideoViewerPath() {
if(metadata == null){
return "";
}

return metadata.get("dam:scene7Domain") + "s7viewers/html5/js/VideoViewer.js";
}

public String getDmVideoServerUrl() {
if(metadata == null){
return "";
}

return metadata.get("dam:scene7Domain") + "is/content/";
}

public Map<String, String> getOverlayDivStyle() {
Map<String, String> divStyles = new HashMap<String, String>();
ValueMap vm = this.resource.getValueMap();

divStyles.put("top" , vm.get("overlayTop", ""));
divStyles.put("left" , vm.get("overlayLeft", ""));
divStyles.put("backgroundColor" , vm.get("overlayBGColor", "#FFFFFF"));
divStyles.put("padding" , vm.get("overlayPadding", "10px 20px 10px 20px"));

return divStyles;
}

public String getDmVideoPath() {
return eaemDMVideo;
}

public String getDmVideoEncode() {
return getDmAccountName() + "/" + eaemDMEncode;
}
}

AEM 6550 - Compose Emails in AEM Assets Console

$
0
0

Goal

Provide Send Mail feature in Assets Console - http://localhost:4502/assets.html, so users can send free form emails about selected assets without leaving the AEM context...

FakeSMTP configured as mail server, for more info check this


Demo | Package Install | Github


Email Modal



Fake SMTP Server

                              Fake SMTP Server intercepting emails sent to localhost on port 25


 

Configure FakeSMTP properties in AEM

                              http://localhost:4502/system/console/configMgr/com.day.cq.mailer.DefaultMailService



Solution

1) Create email template nt:file /apps/eaem-assets-send-mail/mail-templates/send-assets.html with the following code...

Subject: ${subject}

<table style="width:100%" width="100%" bgcolor="#ffffff" style="background-color:#ffffff;" border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="width:100%">${body}</td>
</tr>
</table>


2) Create a servlet apps.experienceaem.assets.SendMailServlet for sending emails...

package apps.experienceaem.assets;


import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.mailer.MessageGatewayService;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.jcr.base.util.AccessControlUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.framework.Constants;

import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

import java.io.IOException;

@Component(
name = "Experience AEM Send Mail Servlet",
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.methods=POST",
"sling.servlet.paths=/bin/experience-aem/send-mail"
}
)
public class SendMailServlet extends SlingAllMethodsServlet {
private final Logger logger = LoggerFactory.getLogger(getClass());

private static String EMAIL_TEMPLATE_PATH = "/apps/eaem-assets-send-mail/mail-templates/send-assets.html";

@Reference
private MessageGatewayService messageGatewayService;

@Override
protected void doPost(final SlingHttpServletRequest req,
final SlingHttpServletResponse resp) throws ServletException, IOException {
ResourceResolver resourceResolver = req.getResourceResolver();

try{
String to = req.getParameter("./to");
String subject = req.getParameter("./subject");
String body = req.getParameter("./body");

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

emailParams.put("subject", subject);
emailParams.put("body", body.replaceAll("\r\n", ""));

sendMail(resourceResolver, emailParams, to);
}catch(Exception e){
logger.error("Error sending email", e);
}
}

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

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

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

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

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

messageGateway.send(email);

return email;
}
}


3) Add the action bar button Send Mail at path /apps/eaem-assets-send-mail/content/send-mail-but with following 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:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
granite:rel="cq-damadmin-admin-actions-send-mail-activator"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/collection/action"
icon="email"
target=".cq-damadmin-admin-childpages"
text="Send Mail"
variant="actionBar">
<data
jcr:primaryType="nt:unstructured"
text="Email sent..."/>
</jcr:root>


4) Add the send mail modal /apps/eaem-assets-send-mail/send-mail-dialog, a form with To, Subject and Body. When user selects assets and clicks on Send Mail button the modal opens with the assets paths added to content field (email body). User can add additional content before clicking Send....

<?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 Send Mail"
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.assets.send.mail]"/>
</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="/bin/experience-aem/send-mail"
foundationForm="{Boolean}true"
maximized="{Boolean}true"
method="post"
novalidate="{Boolean}true"
style="vertical">
<successresponse
jcr:primaryType="nt:unstructured"
jcr:title="Success"
sling:resourceType="granite/ui/components/coral/foundation/form/responses/openprompt"
open="/assets.html"
redirect="/apps/eaem-assets-send-mail/send-mail-dialog.html"
text="Email sent"/>
<items jcr:primaryType="nt:unstructured">
<wizard
jcr:primaryType="nt:unstructured"
jcr:title="Compose"
sling:resourceType="granite/ui/components/coral/foundation/wizard">
<items jcr:primaryType="nt:unstructured">
<container
granite:class="eaem-send-mail-form"
jcr:primaryType="nt:unstructured"
jcr:title="Compose"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<actionbar
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<to
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Comma separated 'To' list..."
fieldLabel="To"
name="./to"/>
<subject
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Enter Subject..."
fieldLabel="Subject"
name="./subject"/>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
cols="15"
fieldDescription="Email body content"
fieldLabel="Content"
name="./body"
rows="22"/>
</items>
</actionbar>
</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="Send"
type="submit"
variant="primary">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="next"/>
</next>
</parentConfig>
</container>
</items>
</wizard>
</items>
</content>
</items>
</body>
</jcr:content>
</jcr:root>


5) Create node /apps/eaem-assets-send-mail/send-mail-dialog/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [dam.gui.actions.coral, eaem.assets.send.mail], String[] property dependencies with value lodash.


6) Create file (nt:file) /apps/eaem-assets-send-mail/send-mail-dialog/js.txt, add

                        dialog-send-mail.css

7) Create file (nt:file) /apps/eaem-assets-send-mail/send-mail-dialog/dialog-send-mail.css, add the following code...

.eaem-send-mail-form{
margin: 10px 40px 0px 40px;
height: 87%;
}

.eaem-send-mail-col1{
width: 30%;
margin-top: 10px;
overflow-y: auto;
}

.eaem-send-mail-col2{
height: 65%;
padding: 1rem;
overflow: hidden;
width: 65%;
margin-top: 30px;
margin-left: 50px;
text-align: center;
background: white;
}

.eaem-send-mail-apply{
margin-top: 20px;
margin-left: 88%;
}

.eaem-send-mail-apply-modal {
width: 50%;
margin-left: -50%;
height: 63%;
margin-top: -50%;
box-sizing: content-box;
z-index: 10100;
}

.eaem-send-mail-apply-modal > iframe {
width: 100%;
height: 100%;
border: 1px solid #888;
}


8) Create file (nt:file) /apps/eaem-assets-send-mail/send-mail-dialog/js.txt, add

                        dialog-send-mail.js

9) Create file (nt:file) /apps/eaem-assets-send-mail/send-mail-dialog/dialog-send-mail.js, add the following code...

(function ($, $document) {
var BUTTON_URL = "/apps/eaem-assets-send-mail/content/send-mail-but.html",
SHARE_ACTIVATOR = "cq-damadmin-admin-actions-adhocassetshare-activator",
SEND_MAIL_SERVLET = "/bin/experience-aem/send-mail",
SEND_MAIL_URL = "/apps/eaem-assets-send-mail/send-mail-dialog.html",
CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
SENDER = "experience-aem", REQUESTER = "requester", $mailModal,
url = document.location.pathname;

if( url.indexOf("/assets.html") == 0 ){
$document.on("foundation-selections-change", addSendMail);
}else if(url.indexOf(SEND_MAIL_URL) == 0){
handleSendMailDialog();
}

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

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

$document.submit(sendMailSentMessage);
}

function sendMailSentMessage(){
var message = {
sender: SENDER,
action: "send"
};

getParent().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 closeModal(event){
event = event.originalEvent || {};

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

var message, action;

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

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

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

if(message.action == "send"){
showAlert("Email sent...", $mailModal.mailSentMessage);
}
}

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

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

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

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

setWidgetValue(form, "[name='./subject']", queryParams.subject);

setWidgetValue(form, "[name='./body']", queryParams.body);
}

function setWidgetValue(form, selector, value){
Coral.commons.ready(form.querySelector(selector), function (field) {
field.value = _.isEmpty(value) ? "" : decodeURIComponent(value);
});
}

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 addSendMail(){
$.ajax(BUTTON_URL).done(addButton);
}

function addButton(html) {
var $eActivator = $("." + SHARE_ACTIVATOR);

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

var $mail = $(html).css("margin-left", "20px").insertBefore($eActivator);

$mail.click(openModal);

$(window).off('message', closeModal).on('message', closeModal);
}

function openModal(){
var actionConfig = ($(this)).data("foundationCollectionAction");

var $items = $(".foundation-selections-item"),
assetPaths = [];

$items.each(function () {
assetPaths.push($(this).data("foundationCollectionItemId"));
});

var body = "Please review the following assets... \n\n" + assetPaths.join("\n");

showMailModal(getModalIFrameUrl("Experience AEM: Review Assets...", body), actionConfig.data.text);
}

function showMailModal(url, mailSentMessage){
var $iframe = $('<iframe>'),
$modal = $('<div>').addClass('eaem-send-mail-apply-modal coral-Modal');

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

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

$mailModal = $modal;

$mailModal.mailSentMessage = mailSentMessage;
}

function getModalIFrameUrl(subject, body){
var url = Granite.HTTP.externalize(SEND_MAIL_URL) + "?" + REQUESTER + "=" + SENDER;

url = url + "&subject=" + encodeURIComponent(subject) + "&body=" + encodeURIComponent(body);

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


AEM 6550 - Extend AEM Asset Picker Card View to select Image Smart Crops and Video Dynamic Renditions

$
0
0


AEM Asset Picker (http://localhost:4502/aem/assetpicker) out of the box does not show Dynamic Renditions for selecting them in third party applications (using iframe). This post is on extending the Asset Picker card view to show (and select) Video Renditions (encodes) and Image Smart Crops...

For showing dynamic renditions in column view check this post


Demo | Package Install | Github


Configure Image Profiles



Dynamic Renditions in Assets Details View



Extension - Dynamic Renditions in Asset Picker Browse



Extension - Dynamic Renditions in Asset Picker Search



Extension - JSON to Parent Window on Select



Solution

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-asset-selector-card-show-dyn-renditions

2) Create the following nt:file /apps/eaem-asset-selector-card-show-dyn-renditions/smart-crop-renditions/smart-crop-renditions.jsp to return the smart crop renditions of an image as JSON

<%@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" %>

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

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

response.setContentType("application/json");

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

dynResource = items.next();

dynVM = dynResource.getValueMap();

String name = String.valueOf(dynVM.get("breakpoint-name"));

dynRendition.put("type", "IMAGE");
dynRendition.put("name", name);
dynRendition.put("image", dynVM.get("copyurl"));
dynRendition.put("url", dynVM.get("copyurl"));

dynRenditions.put(name, dynRendition);
}

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


3) Set the datasource for Image Smart Crops /apps/eaem-asset-selector-card-show-dyn-renditions/smart-crop-renditions/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource


4) Create the following nt:file /apps/eaem-asset-selector-card-show-dyn-renditions/video-dyn-renditions/video-dyn-renditions.jsp to return the encodes of a video as JSON

<%@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="org.apache.commons.lang3.StringUtils" %>

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

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

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

JSONObject dynRenditions = new JSONObject();

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

DynamicMediaRenditionProvider dmRendProvider = sling.getService(DynamicMediaRenditionProvider.class);
String s7Domain = String.valueOf(asset.getMetadata("dam:scene7Domain"));
String scene7FileAvs = String.valueOf(asset.getMetadata("dam:scene7FileAvs"));

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

List<Rendition> dmRenditions = dmRendProvider.getRenditions(asset, rules);
JSONObject dynRendition = new JSONObject();
String image = null, avsName = scene7FileAvs.substring(scene7FileAvs.lastIndexOf("/") + 1);

dynRendition.put("type", "VIDEO");
dynRendition.put("url", getScene7Url(s7Domain, scene7FileAvs));
dynRendition.put("image", getRendThumbnail(s7Domain, scene7FileAvs));
dynRendition.put("name", avsName);

dynRenditions.put(avsName, dynRendition);

String previewUrl = null;

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

image = dmRendition.getPath();

if(image.endsWith(".mp4")){
image = image.substring(0, image.lastIndexOf(".mp4"));

if(StringUtils.isEmpty(previewUrl)){
previewUrl = getPreviewUrl(s7Domain, dmRendition.getPath());
previewUrl = previewUrl.substring(0, previewUrl.lastIndexOf(".mp4"));
}
}

dynRendition.put("type", "VIDEO");
dynRendition.put("name", dmRendition.getName());
dynRendition.put("image", getRendThumbnail(s7Domain, image));
dynRendition.put("url", getScene7Url(s7Domain, dmRendition.getPath()));
dynRendition.put("preview", previewUrl);

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

((JSONObject)dynRenditions.get(avsName)).put("preview", previewUrl);

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){
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";
}
%>


5) Create node /apps/eaem-asset-selector-card-show-dyn-renditions/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.gui.damadmin.assetselector], String[] property dependencies with value lodash.


6) Create file (nt:file) /apps/eaem-asset-selector-card-show-dyn-renditions/clientlib/js.txt, add

                        

                                            card-view-show-dyn-renditions.js


7) Create file (nt:file) /apps/eaem-asset-selector-card-show-dyn-renditions/clientlib/card-view-show-dyn-renditions.js, add the following code

(function($, $document){
var CORAL_COLUMNVIEW_PREVIEW = "coral-columnview-preview",
THUMB_PATH = "/_jcr_content/renditions/cq5dam.thumbnail.48.48.png",
EAEM_DATA_ASSET_PATH = "data-eaem-asset-path",
EAEM_RENDITION_DATA = "data-eaem-rendition",
EAEM_RENDITION_FIELD = "eaem-rendition-name",
EAEM_CARD_DYN_RENDS_BLOCK = ".eaem-card-dyn-rends-block",
EAEM_CARD_ASSETS_BLOCK = ".eaem-card-assets-block",
SEARCH_RESULTS_CONTAINER = "#granite-pickerdialog-search-result-content",
EAEM_DONE_ACTION = "EAEM_DONE",
FUI = $(window).adaptTo("foundation-ui"),
BROWSE_CARDS_CONTAINER = ".foundation-layout-panel-content",
GET_SMART_CROPS_URL = "/apps/eaem-asset-selector-card-show-dyn-renditions/smart-crop-renditions/renditions.html",
GET_VIDEO_RENDS_URL = "/apps/eaem-asset-selector-card-show-dyn-renditions/video-dyn-renditions/renditions.html";

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

$document.on("foundation-selections-change", function(){
if(!(isBrowseCardsView() || isSearchCardsView())){
return;
}

FUI.wait();

_.defer(handleCardViewSelections);
});

function registerSelectListener(){
$document.off('click', '.asset-picker-done');

$(document).on("click", ".asset-picker-done", function(e) {
e.stopImmediatePropagation();
exportAssetInfo(e);
});
}

function exportAssetInfo(e){
var message = {
config: {
action: EAEM_DONE_ACTION
},
data: []
};

var $selItem, selected;

if(isSearchCardsView() || isBrowseCardsView()){
$selItem = $(".eaem-card-dyn-rends-block coral-masonry-item.is-selected");

if(_.isEmpty($selItem)){
$selItem = $(".eaem-card-assets-block coral-masonry-item.is-selected");
}
}else{
$selItem = $("coral-columnview-item.is-selected");
}

var renditionData = $selItem.attr(EAEM_RENDITION_DATA);

if(!renditionData){
let path = $selItem.attr("data-foundation-collection-item-id"),
name = path.substring(path.lastIndexOf("/") + 1),
type = $selItem.find("coral-card-context").html();

selected = {
type: type ? type.trim() : "",
image: path,
name: name,
aemPath: path,
url : ""
}
}else{
selected = JSON.parse(renditionData);
selected["aemPath"] = $selItem.data("eaemAssetPath");
}

message.data.push(selected);

console.log(message);

getParent().postMessage(JSON.stringify(message), $(".assetselector-content-container").data("targetorigin"));

if(isBrowseCardsView()){
$("coral-masonry-item.is-selected").removeClass("is-selected");
}
}

function handleCardViewSelections(){
var $selItem = $("coral-masonry-item.is-selected");

if(_.isEmpty($selItem)){
hideDynRenditionsContainer();
return;
}

$(".asset-picker-done")[0].disabled = false;

var $dynRendsContainer;

if(isSearchCardsView()){
$dynRendsContainer = $(SEARCH_RESULTS_CONTAINER).find(EAEM_CARD_DYN_RENDS_BLOCK);
}else if(isBrowseCardsView()){
$dynRendsContainer = $(BROWSE_CARDS_CONTAINER).find(EAEM_CARD_DYN_RENDS_BLOCK);
}

if(_.isEmpty($dynRendsContainer)){
$dynRendsContainer = createCardRenditionsContainer();
}else{
if(isBrowseCardsView()){
showDynRenditionsContainer();
}
}

var assetType = $selItem.find("coral-card-context").html(),
rendsUrl = isImage(assetType) ? GET_SMART_CROPS_URL : GET_VIDEO_RENDS_URL,
assetPath = $selItem.attr("data-granite-collection-item-id");

if(!assetPath){
FUI.clearWait();
hideDynRenditionsContainer();
return;
}

rendsUrl = rendsUrl + assetPath;

$.ajax( { url: rendsUrl, async: false } ).done(function(data){
var html = '<coral-masonry>';

_.each(data, function(dynRendition, dynName){
html = html + getCardDynamicRenditionHtml(dynRendition, assetPath);
});

html = html + '</coral-masonry>';

$dynRendsContainer.html(html);

_.defer(handCardDynRendSelection);

FUI.clearWait();
});
}

function getCardDynamicRenditionHtml(dynRendition, assetPath) {
return '<coral-masonry-item data-foundation-collection-item-id="' + dynRendition.image + '"' + EAEM_DATA_ASSET_PATH + '="' + assetPath
+ '"' + EAEM_RENDITION_DATA + '="' + JSON.stringify(dynRendition).replace(/\"/g, """) + '">' +
'<coral-card>' +
'<coral-card-asset>' +
'<img src="' + dynRendition.image + '">' +
'</coral-card-asset>' +
'<coral-card-content>' +
'<coral-card-context>Dynamic Rendition</coral-card-context>' +
'<coral-card-title>' + dynRendition.name + '</coral-card-title>' +
'</coral-card-content>' +
'</coral-card>' +
'</coral-masonry-item>' ;
}

function showDynRenditionsContainer(){
$(EAEM_CARD_DYN_RENDS_BLOCK).show();

$(EAEM_CARD_ASSETS_BLOCK).css("width", "85%");
}

function hideDynRenditionsContainer(){
FUI.clearWait();

$(EAEM_CARD_DYN_RENDS_BLOCK).hide();

$(EAEM_CARD_ASSETS_BLOCK).css("width", "100%");
}

function handCardDynRendSelection(){
var $dynRends = $(".eaem-card-dyn-rends-block").find("coral-masonry-item");

$dynRends.click(function(){
var $dynRend = $(this);

$dynRends.removeClass("is-selected");

$dynRend.addClass("is-selected");
})
}

function createCardRenditionsContainer(){
var $container = isSearchCardsView() ? $(SEARCH_RESULTS_CONTAINER) : $(BROWSE_CARDS_CONTAINER);

$container.wrapInner("<div style='display:block'><div class='eaem-card-assets-block'></div></div>");

return $("<div class='eaem-card-dyn-rends-block'></div>").appendTo($container.children("div"));
}

function isBrowseCardsView(){
return ( ($(".foundation-layout-panel-content coral-masonry").length > 0)
&& !($(".foundation-layout-panel-content")[0].hasAttribute("hidden")));
}

function isSearchCardsView() {
return ($(SEARCH_RESULTS_CONTAINER).length > 0);
}

function isImage(typeValue){
if(!typeValue){
return false;
}

return (typeValue.trim() == "IMAGE");
}

function isVideo(typeValue){
if(!typeValue){
return false;
}
return (typeValue.trim() == "MULTIMEDIA");
}

function getParent() {
if (window.opener) {
return window.opener;
}
return parent;
}
}(jQuery, jQuery(document)));

AEM 6550 - Touch UI Rich Text Editor, configure emptyText

$
0
0

Goal

Configure emptyText property in Rich Text Editor (RTE) configuration to show placeholder text before entering content...

Another way is probably using CSS hack like below and setting data-eaem-placeholder attribute on the RTE div (however, it does not show the placeholder when there are empty tags in RTE, with no text content)...

[data-cq-richtext-editable="true"]:empty:before {
content:attr(data-eaem-placeholder);
color:gray
}


Demo | Package Install | Github


Configure emptyText

                   /apps/core/wcm/components/text/v2/text/cq:dialog/content/items/tabs/items/properties/items/columns/items/column/items/text

                     emptyText=Experience AEM - This richtext editor is empty, add some text...




Placeholder Text in RTE


Solution

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-rte-empty-text

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

3) Create file (nt:file) /apps/eaem-rte-empty-text/clientlib/css.txt, add


                        rte-empty-text.css

4) Create file (nt:file) /apps/eaem-rte-empty-text/clientlib/rte-empty-text.css, add the following code

.eaem-rte-placeholder{
color: gray;
display: block;
}


5) Create file (nt:file) /apps/eaem-rte-empty-text/clientlib/js.txt, add

                        rte-empty-text.js

6) Create file (nt:file) /apps/eaem-rte-empty-text/clientlib/rte-empty-text.js, add the following code

(function($, $document){
var RTE_SEL = "[data-cq-richtext-editable='true']",
DATA_PLACE_HOLDER = "data-eaem-rte-placeholder",
PLACE_HOLDER_CSS = "eaem-rte-placeholder";

$document.on("dialog-ready", function(){
$(RTE_SEL).each(addPlaceHolderTextInData);
});

$document.on("click", ".cq-dialog-submit", handleBeforeSubmit);

function handleBeforeSubmit(e){
e.stopPropagation();
e.preventDefault();

$(RTE_SEL).each(clearPlaceholderText);

var $form = $(this).closest("form.foundation-form"),
$rteInputs = $form.find("[data-cq-richtext-input='true']");

_.each($rteInputs, function(input){
var $input = $(input),
val = $input.val();

if(!val.includes(PLACE_HOLDER_CSS)){
return;
}

$input.val(getPlaceHolderTextRemovedFromRTEInput(val));
});

$form.submit();
}

function getPlaceHolderTextRemovedFromRTEInput(val){
//todo: when there is no text, placeholder is getting saved, find a better way to remove
return val.substring(val.indexOf("</div>") + 6);
}

function addPlaceholderText(){
var $rte = $(this),
text = $rte.text().trim(),
placeholderText = $rte.attr(DATA_PLACE_HOLDER);

if(!placeholderText){
return;
}

if(text){
$rte.find("." + PLACE_HOLDER_CSS).remove();
}else{
$rte.prepend(getPlaceholder(placeholderText));
}
}

function clearPlaceholderText(){
$(this).find("." + PLACE_HOLDER_CSS).remove();
}

function getPlaceholder(text){
return "<div class='" + PLACE_HOLDER_CSS + "'>" + text + "</div>";
}

function addPlaceHolderTextInData(index, rte){
var $rte = $(rte),
configPath = $rte.attr("data-config-path");

if(!configPath){
return;
}

$.ajax(configPath).done(function(data){
var emptyText = data["emptyText"];

if(!emptyText){
return;
}

$rte.attr(DATA_PLACE_HOLDER, emptyText);

$rte.change(addPlaceholderText);

$rte.click(clearPlaceholderText);

addPlaceholderText.call($rte[0]);
})
}
}(jQuery, jQuery(document)));

AEM 6550 - Sites Dialog Add Boolean Typehints before Submit

$
0
0

Goal

For saving true or false values as Boolean and not String, you can add @TypeHint properties in dialog like below... 

<openInNewWindow
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
checked="{Boolean}true"
name="./openInNewWindow"
text="Open in New Window"
uncheckedValue="{Boolean}false"
value="{Boolean}true"/>
<openInNewWindowType
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/hidden"
name="./openInNewWindow@TypeHint"
value="Boolean"/>


However if you'd like to avoid adding TypeHint properties in dialog (say too many checkboxes in dialog) but 'd like to have all checkbox values saved as Boolean in CRX, by automatically adding TypeHint before Save, add the following extension...


Demo | Package Install | Github


Product


Extension


Solution

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-save-checkboxes-as-boolean

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

3) Create file (nt:file) /apps/eaem-save-checkboxes-as-boolean/clientlib/js.txt, add

                        save-checkbox-as-boolean.js

4) Create file (nt:file) /apps/eaem-save-checkboxes-as-boolean/clientlib/save-checkbox-as-boolean.js, add the following code

(function($, $document){
var CHECK_BOX_SEL = "form.cq-dialog input[type='checkbox']";

$document.on("click", ".cq-dialog-submit", convertStringToBoolean);

function convertStringToBoolean(event){
event.stopPropagation();
event.preventDefault();

$(CHECK_BOX_SEL).each(addTypeHint);

$("form.cq-dialog").submit();
}

function addTypeHint(){
var $checkbox = $(this),
value = $checkbox.val(),
$form = $("form.cq-dialog");

if( (value != "true") && (value != "false")){
return;
}

var typeHintName = $checkbox.attr("name") + "@TypeHint";

$form.append($("<input type='hidden'/>").attr("name", typeHintName).attr("value", "Boolean"));
}
}(jQuery, jQuery(document)));


AEM 6550 - SPA Editor Refresh Component (ReRender) on Dialog Update

$
0
0

Goal

Refresh & Render the SPA component with updated model data when a user adds new content in dialog...

Demo | Package Install | Github




Solution

1) Updating the component is a 2 step process...

                                            a) Trigger a custom event from the SPA editor with latest dialog data...

                                            b)  Listen to the event in component render script and update...


Trigger Custom Event

1) Add a client library to extend editable action EditableActions.REFRESH and pass the updated model in a custom event eaem-spa-component-refresh-event. Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-sites-spa-how-to-react/clientlibs


2) Create node /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.editor], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/js.txt, add

                        refresh-component.js

4) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/refresh-component.js, add the following code

(function($, $document){
var EAEM_COMPONENTS = "eaem-sites-spa-how-to-react/",
EAEM_SPA_COMP_REFRESH_EVENT = "eaem-spa-component-refresh-event";

$document.on("cq-editables-loaded", overrideSPAImageCompRefresh);

function overrideSPAImageCompRefresh(){
var _origExec = Granite.author.edit.EditableActions.REFRESH.execute;

Granite.author.edit.EditableActions.REFRESH.execute = function(editable, config){
if(editable.type.startsWith(EAEM_COMPONENTS)){
$.ajax(editable.slingPath).done(function(compData){
sendComponentRefreshEvent(editable, compData);
});
}

return _origExec.call(this, editable, config);
};
}

function sendComponentRefreshEvent(editable, compData){
let event = new CustomEvent(EAEM_SPA_COMP_REFRESH_EVENT, {
detail: {
type: editable.type,
path: editable.path,
slingPath: editable.slingPath,
data: compData
}
});

window.dispatchEvent(event);
}
}(jQuery, jQuery(document)));



Add Custom Event Listener

5) In the React render script add a listener in componentDidMount() function to check if the event is for this component, update this.props with  latest model data and call this.forceUpdate() to update the component display (without refreshing page...)

import { MapTo } from '@adobe/cq-react-editable-components';
import DOMPurify from 'dompurify';
import React, { Component } from 'react';
import {Link} from "react-router-dom";

const ImageEditConfig = {
emptyLabel: 'Image - Experience AEM',

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

class Image extends Component {
componentDidMount() {
//todo check for wcmmode
window.parent.addEventListener("eaem-spa-component-refresh-event", (event => {
if( !event.detail || (event.detail.type !== this.props.cqType)){
return;
}

Object.assign(this.props, event.detail.data);

this.forceUpdate();
}).bind(this));
}

get imageHTML() {
const imgStyles = {
"display": 'block',
"margin-left": 'auto',
"margin-right": 'auto'
};

return (
<div>
<Link to={this.props.imageLink}>
<img src={this.props.fileReference} style={imgStyles}/>
</Link>
</div>
);
}

render() {
return this.imageHTML;
}
}

export default MapTo('eaem-sites-spa-how-to-react/components/image')(Image,ImageEditConfig);

AEM 6550 - Stream Binary or Text Content of Provided URL thru AEM for bypassing CORS Issues

$
0
0

Goal

If you are encountering CORS (Access-Control-Allow-Origin) issues when making client side ajax calls to another server (which does not provide Access-Control-Allow-Origin header in response) from browser, a quick alternative is to proxy the URL from AEM... the following post proxies images or xml content (useful when accessing Scene7 Render Server images...)

Github

                     http://localhost:4502/bin/experience-aem/proxy?format=XML&url=http%3A%2F%2Fsample.scene7.com%2Fir%2Frender%2FS7trainRender%2Fninewest_shoe%3Freq%3Dcontents




Solution

Add a servlet apps.experienceaem.assets.ProxyContentServlet to read the url, format and proxy the response through AEM...

package apps.experienceaem.assets;

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.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.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.*;

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

private static String IMAGE = "IMAGE";
private static String XML = "XML";

@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();

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

if(StringUtils.isEmpty(url)){
throw new Exception("Empty url");
}

if(StringUtils.isEmpty(format)){
format = IMAGE;
}

if(format.equalsIgnoreCase(IMAGE)){
streamImage(response, url, fileName);
}else if(format.equalsIgnoreCase(XML)){
streamXML(response, url);
}
} catch (Exception e) {
log.error("Could not getting binary response", e);
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

private void streamImage(SlingHttpServletResponse response, String url, String fileName) throws Exception{
if(StringUtils.isEmpty(fileName)){
fileName = "eaem.jpg";
}

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

writeContent(response, url);
}

private void streamXML(SlingHttpServletResponse response, String url) throws Exception{
response.setContentType("application/xml");

writeContent(response, url);
}

private void writeContent(SlingHttpServletResponse response, String url) throws Exception{
byte[] image = Request.Get(url).execute().returnContent().asBytes();

InputStream in = new ByteArrayInputStream(image);

OutputStream out = response.getOutputStream();

IOUtils.copy(in, out);

out.close();

in.close();
}
}

AEM 6550 - React Type Script SPA Composite Positioning Container Component

$
0
0

Goal

Create a React SPA Positioning Container for Component Overlays. The container provides authoring dialog interface for adding backgrounds with color, image, video etc.. adding opacity, inner components alignment, positioning, width, height and various other features for creating a free form nested component layout....

Thank De Shauné Elder for design reference https://www.awesomescreenshot.com/video/832666?key=18f8d1f8822aa95ee43ce4c0b20bb837

Demo | Package Install | Github


Container Editing


Preview



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-spa-how-to-react" -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-spa-how-to-react/components/spa
                                                          /apps/eaem-sites-spa-how-to-react/components/page
                                                          /apps/eaem-sites-spa-how-to-react/components/text
 

3) Create component /apps/eaem-sites-spa-how-to-react/components/positioning-container extending core/wcm/components/container/v1/container. Add the component formatting options in tabs Background, Format and Colors





4) Create the custom widget for opacity used in dialog - /apps/eaem-sites-spa-how-to-react/sites/extensions/slider 




5) Add the following logic in file /apps/eaem-sites-spa-how-to-react/sites/extensions/slider/slider.jsp for opacity...

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

<%@page session="false"
import="org.apache.commons.lang3.StringUtils,
com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Config,
com.adobe.granite.ui.components.Field,
com.adobe.granite.ui.components.Tag" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%
Config cfg = cmp.getConfig();

SlingHttpServletRequest thisRequest = slingRequest;

Resource dialog = thisRequest.getResourceResolver().getResource(thisRequest.getRequestPathInfo().getSuffix());
ValueMap vm = slingRequest.getResource().getValueMap();

String name = cfg.get("name", String.class);
String sliderValue = dialog.getValueMap().get(name, "50");

Tag tag = cmp.consumeTag();

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

attrs.add("name", name);
attrs.add("value", sliderValue);
attrs.add("min", cfg.get("min", Double.class));
attrs.add("max", cfg.get("max", Double.class));
attrs.add("step", cfg.get("step", String.class));

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>
<coral-slider style="width:100%; margin: 0"<%= attrs.build() %>></coral-slider>
<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>
<div class="eaem-dialog-slider">
<span><%=sliderValue%>%</span>
</div>


6) Create the custom widget for content alignment used in dialog - /apps/eaem-sites-spa-how-to-react/sites/extensions/alignment




7) Add the following logic in file /apps/eaem-sites-spa-how-to-react/sites/extensions/alignment/alignment.jsp for alignment...

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

<%@page session="false"
import="org.apache.commons.lang3.StringUtils,
com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Config,
com.adobe.granite.ui.components.Field,
com.adobe.granite.ui.components.Tag" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%
Config cfg = cmp.getConfig();

SlingHttpServletRequest thisRequest = slingRequest;
Resource dialog = thisRequest.getResourceResolver().getResource(thisRequest.getRequestPathInfo().getSuffix());

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

ValueMap vm = dialog.getValueMap();
String value = vm.get(name, "Center");

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>

<div class="eaem-dialog-content-align">
<input type="hidden" name="<%=name%>" value="<%=value%>"/>

<div>Center</div>

<coral-icon icon="chevronUp" size="M" data-content-align="Top"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Top</coral-tooltip>
<coral-icon icon="chevronDown" size="M" data-content-align="Bottom"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Bottom</coral-tooltip>
<coral-icon icon="chevronDoubleLeft" size="M" data-content-align="Extreme Left"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Extreme Left</coral-tooltip>
<coral-icon icon="chevronLeft" size="M" data-content-align="Left"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Left</coral-tooltip>
<coral-icon icon="chevronRight" size="M" data-content-align="Right"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Right</coral-tooltip>
<coral-icon icon="chevronDoubleRight" size="M" data-content-align="Extreme Right"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Extreme Right</coral-tooltip>
<coral-icon icon="chevronUpDown" size="M" data-content-align="Center"></coral-icon>
<coral-tooltip target="_prev" variant="info" role="tooltip" style="display: none;" placement="top">Center</coral-tooltip>
</div>
<coral-icon class="coral-Form-fieldinfo" icon="infoCircle" size="S"></coral-icon>
<coral-tooltip target="_prev" placement="left" variant="info" role="tooltip" style="display: none;">
<coral-tooltip-content><%=fieldDesc%></coral-tooltip-content>
</coral-tooltip>
</div>

8) Create a client library /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions with categories = [cq.authoring.editor] and dependencieslodash for the custom widgets clientside execution...

9) Create clientlib js file /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/js.txt with the following entry

                                                          positioning-container-dialog.js

10) Add the js logic in /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/positioning-container-dialog.js 

(function($, $document){
var DIALOG_SLIDER = ".eaem-dialog-slider",
DIALOG_CONTENT_ALIGN = ".eaem-dialog-content-align",
DIALOG_FIELD_SELECTED = "eaem-dialog-content-selected";

$document.on("dialog-ready", initPositioningContainerDialog);

function initPositioningContainerDialog(){
addSliderListener();

addContentAlignmentListener();
}

function addSliderListener(){
var $sliders = $(DIALOG_SLIDER);

$sliders.each(function(){
var $sliderValue = $(this),
$slider = $sliderValue.prev(".coral-Form-fieldwrapper").find("coral-slider");

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

$slider.on("change", function(){
$sliderValue.html($(this).val() + "%");
});
});
}

function addContentAlignmentListener(){
var $contentAlignContainer = $(DIALOG_CONTENT_ALIGN),
$contentAlignDisplay = $contentAlignContainer.find("div"),
$contentAlign = $("[name='./contentAlignment']");

addInitialPositions();

$contentAlignContainer.find("coral-icon").click(function(){
$(this).toggleClass(DIALOG_FIELD_SELECTED);

calculatePositioning();
});

function addInitialPositions(){
var alignments = $contentAlign.val();

$contentAlignDisplay.html(alignments);

_.each(alignments.split(","), function(alignment){
var $icon = $contentAlignContainer.find("[data-content-align='" + alignment.trim() + "']");

$icon.addClass(DIALOG_FIELD_SELECTED);
})
}

function calculatePositioning(){
var $alignIcons = $contentAlignContainer.find("coral-icon." + DIALOG_FIELD_SELECTED),
position = "";

$alignIcons.each(function(){
position = position + $(this).data("content-align") + ", ";
});

if(position.includes(",")){
position = position.substring(0, position.lastIndexOf(","));
}

position = position.trim();

if(!position){
position = "Center";
}

$contentAlignDisplay.html(position);
$contentAlign.val(position);
}
}
}(jQuery, jQuery(document)));


11) For the data required by SPA component interface add sling model com.eaem.core.models.EAEMPositioningContainerModelImpl with the following code, returning dialog data as plain JSON...

package com.eaem.core.models;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ContainerExporter;
import com.day.cq.wcm.foundation.model.responsivegrid.ResponsiveGrid;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
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.ScriptVariable;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Model(
adaptables = {SlingHttpServletRequest.class},
adapters = {ContainerExporter.class, ComponentExporter.class},
resourceType = {"eaem-sites-spa-how-to-react/components/positioning-container"}
)
@Exporter(
name = "jackson",
extensions = {"json"}
)
@JsonSerialize(as = EAEMPositioningContainerModel.class)
public class EAEMPositioningContainerModelImpl extends ResponsiveGrid implements EAEMPositioningContainerModel{
@ScriptVariable
private Resource resource;

@PostConstruct
protected void initModel() {
super.initModel();
}

public Map<String, Object> getBackgroundProps(){
Map<String, Object> backgroundDivProps = new LinkedHashMap<String, Object>();

ValueMap vm = resource.getValueMap();
String overlayOpacity = vm.get("overlayOpacity", "100");

backgroundDivProps.put("backgroundHeight", vm.get("backgroundHeight", "500px"));
backgroundDivProps.put("backgroundWidth", vm.get("backgroundWidth", "INSET"));
backgroundDivProps.put("overlayOpacity", Float.parseFloat(overlayOpacity) / 100);
backgroundDivProps.put("backgroundType", vm.get("backgroundType", "NONE"));
backgroundDivProps.put("backgroundImage", vm.get("backgroundImage", ""));
backgroundDivProps.put("backgroundColor", vm.get("backgroundColor", ""));

return backgroundDivProps;
}

public Map<String, Object> getSectionProps(){
Map<String, Object> sectionProps = new LinkedHashMap<String, Object>();
ValueMap vm = resource.getValueMap();

sectionProps.put("sectionHeight", vm.get("sectionHeight", ""));
sectionProps.put("contentWidth", vm.get("contentWidth", ""));
sectionProps.put("sectionBGColor", vm.get("sectionBGColor", ""));
sectionProps.put("contentAlignment", vm.get("contentAlignment", "Center"));

return sectionProps;
}
}


12) At this point you should be able to access the component dialog data using sling model url eg. 

                                                          http://localhost:4502/content/eaem-sites-spa-how-to-react/us/en/eaem-home/jcr:content/root/responsivegrid/positioning_containe.model.json



13) On the React front end side, add necessary configuration for typescript support in eaem-sites-react-spa-positioning-container\ui.frontend\package.json and run npm install

                                  "dependencies": {
                                   "@types/jest": "^26.0.0",
                                   "@types/node": "^14.0.13",
                                   "@types/react": "^16.9.38",
                                   "@types/react-dom": "^16.9.8",
                                   "typescript": "^3.9.5"
                                   ......
}

14) Add the TS file eaem-sites-react-spa-positioning-container\ui.frontend\src\components\PositioningContainer\PositioningContainer.tsx for necessary container component positioning logic...

import React from "react";
import CSS from 'csstype';
import { MapTo, Container } from "@adobe/cq-react-editable-components";

class EAEMPositioningContainer extends Container {
OVERLAY_POSITION = {
TOP: "10%",
BOTTOM: "80%",
LEFT: "20%",
EXTREME_LEFT: "5%",
RIGHT: "20%",
EXTREME_RIGHT: "5%"
};

constructor(props: any) {
super(props);

//@ts-ignore
this.props = props;
}

get childComponents() {
return super.childComponents;
}

get placeholderComponent() {
return super.placeholderComponent;
}

get containerProps() {
let containerProps = super.containerProps;

//@ts-ignore
let rhProps = this.props;

rhProps.backgroundProps = rhProps.backgroundProps || {};
rhProps.sectionProps = rhProps.sectionProps || {};

let bgProps = rhProps.backgroundProps;

const bgStyles: CSS.Properties = {
zIndex: 0,
position: "relative"
};

bgStyles.width = "100%";
bgStyles.height = bgProps.backgroundHeight;
bgStyles.backgroundColor = bgProps.backgroundColor;
bgStyles.opacity = bgProps.overlayOpacity;

if (bgProps.backgroundType == "IMAGE"&& bgProps.backgroundImage) {
bgStyles.backgroundImage = 'url("' + bgProps.backgroundImage + '")';
//bgStyles.backgroundRepeat = "no-repeat";
}

containerProps.style = bgStyles;

return containerProps;
}

get sectionStyles() {
//@ts-ignore
let rhProps = this.props;

let sectionProps = rhProps.sectionProps;

const sectionStyles: CSS.Properties = {
zIndex: 1,
position: "absolute"
};

sectionStyles.backgroundColor = sectionProps.sectionBGColor || undefined;
sectionStyles.height = sectionProps.sectionHeight || undefined;

if (sectionProps.contentWidth) {
sectionStyles.width = sectionProps.contentWidth;
sectionStyles.textAlign = "center";
}

let contentAlignment = sectionProps.contentAlignment || "";

if (contentAlignment == "Center") {
sectionStyles.top = "50%";
sectionStyles.left = "50%";
sectionStyles.transform = "translate(-50%, -50%)";
} else {
contentAlignment = contentAlignment.split(",");

contentAlignment.map((alignment: string) => {
alignment = alignment.trim();

if (alignment == "Top") {
sectionStyles["top"] = this.OVERLAY_POSITION.TOP;
} else if (alignment == "Bottom") {
sectionStyles["top"] = this.OVERLAY_POSITION.BOTTOM;
} else if (alignment == "Extreme Left") {
sectionStyles["left"] = this.OVERLAY_POSITION.EXTREME_LEFT;
} else if (alignment == "Left") {
sectionStyles["left"] = this.OVERLAY_POSITION.LEFT;
} else if (alignment == "Extreme Right") {
sectionStyles["right"] = this.OVERLAY_POSITION.EXTREME_RIGHT;
} else if (alignment == "Right") {
sectionStyles["right"] = this.OVERLAY_POSITION.RIGHT;
}
});
}

return sectionStyles;
}

render() {
return (
<div {...this.containerProps}>
<div style={this.sectionStyles}>
{this.childComponents}
{this.placeholderComponent}
</div>
</div>
);
}
}

export default MapTo("eaem-sites-spa-how-to-react/components/positioning-container")(
EAEMPositioningContainer
);

15) Add the PositioningContainer.tsx path in eaem-65-extensions\eaem-sites-react-spa-positioning-container\ui.frontend\src\components\import-components.js

                                           import './Page/Page';
                                   import './Text/Text';
                                   import './PositioningContainer/PositioningContainer';


Viewing all 525 articles
Browse latest View live


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