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

AEM 6550 - React SPA Carousel with Nested Composite Containers as Slides

$
0
0

Goal

Create a React SPA Generic Carousel using Slick JS library. The carousel component can be used to play slides with simple components (text, image etc..) or complex containers. For a sample nested composite positioning components containercheck this post

Demo | Package Install | Github


Carousel Configuration


View as Published


Solution


1) Create the project structure (for both React SPA and traditional 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) Add a client library /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-vendor with categories = [eaem-sites-spa-how-to-react.vendor], in your project to serve the slick JS library


4) Add the clienlib-vendor as a dependency for site base clientlib /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-base by adding the property dependencieseaem-sites-spa-how-to-react.vendor


5) Add the carousel component /apps/eaem-sites-spa-how-to-react/components/composite-container-carousel extending core/wcm/components/carousel/v1/carousel having an additional dialog property collpaseSlidesInEdit. Used only during authoring process (not View As Published) this checkbox is for collapsing all slides to save screen space say when other components on the page are being edited. When unchecked it shows all slides for quicker carousel authoring....


6) Create the React SPA Typescript file for rendering, eaem-sites-react-spa-carousel\ui.frontend\src\components\CompositeContainerCarousel\CompositeContainerCarousel.tsx with following code...


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

class CompositeContainerCarousel extends Container {
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;

containerProps.ref = "eaemSlickSlider";

return containerProps;
}

componentDidUpdate() {
//@ts-ignore
let eaemProps = this.props;

if (!eaemProps.isInEditor) {
return;
}

fetch(eaemProps.cqPath + ".json").then(res => res.json())
.then((rData) => {
if (rData.collpaseSlidesInEdit == "true") {
window.location.reload();
}
});
}

attachSlick() {
//@ts-ignore
let eaemProps = this.props;

if (!eaemProps.isInEditor) {
//@ts-ignore
$(this.refs.eaemSlickSlider).slick();
} else {
fetch(eaemProps.cqPath + ".json").then(res => res.json())
.then((rData) => {
if (rData.collpaseSlidesInEdit == "true") {
//@ts-ignore
$(this.refs.eaemSlickSlider).slick();
}
});
}
}

componentDidMount() {
this.attachSlick();
}

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

export default MapTo("eaem-sites-spa-how-to-react/components/composite-container-carousel")(
CompositeContainerCarousel
);

7) Add the CompositeContainerCarousel.tsx path in eaem-65-extensions\eaem-sites-react-spa-carousel\ui.frontend\src\components\import-components.js

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



AEM 6550 - Query Builder Predicate Evaluator for searching both PSB (PhotoShopBig) and PSD (PhotoShopDocument)

$
0
0

Goal

Assets Search filter for searching both PSB (PhotoShop Big) with mime type application/vnd.3gpp.pic-bw-small and PSD (Photoshop Doucment) with mime type image/vnd.adobe.photoshop, by selecting the filter File Type> Images> Bitmaps> Adobe Photoshop

Github



Solution

Create a predicate evaluator apps.eaem.PhotoshopPredicateEvaluator extending com.day.cq.search.eval.JcrPropertyPredicateEvaluator to create a xpath query something like...

/jcr:root/content/dam/eaem//element(*, dam:Asset)[(jcr:content/metadata/dc:format = 'image/vnd.adobe.photoshop' or jcr:content/metadata/dc:format = 'application/vnd.3gpp.pic-bw-small')]


package com.eaem;

import com.day.cq.search.Predicate;
import com.day.cq.search.eval.EvaluationContext;
import com.day.cq.search.eval.JcrPropertyPredicateEvaluator;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.annotations.Component;

import java.io.StringWriter;

@Component(
factory = "com.day.cq.search.eval.PredicateEvaluator/property"
)
public class PhotoshopPredicateEvaluator extends JcrPropertyPredicateEvaluator {
private String DC_FORMAT_METADATA_PREFIX = "(jcr:content/metadata/@dc:format";
private String PSD_MIME_TYPE = "'image/vnd.adobe.photoshop'";
private String PSB_MIME_TYPE = "'application/vnd.3gpp.pic-bw-small'";

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

if(StringUtils.isEmpty(xPathExpr) || !xPathExpr.startsWith(DC_FORMAT_METADATA_PREFIX)){
return xPathExpr;
}

String value = xPathExpr.substring(xPathExpr.indexOf("=") + 1, xPathExpr.lastIndexOf(")"));

if(!PSD_MIME_TYPE.equals(value.trim())){
return xPathExpr;
}

StringWriter sw = new StringWriter();

String firstExpr = xPathExpr.substring(0, xPathExpr.lastIndexOf(")"));

sw.append(firstExpr).append(" or jcr:content/metadata/@dc:format = " + PSB_MIME_TYPE + ")");

return sw.toString();
}
}


AEM 6550 - Omni Search filter for searching PSB (PhotoShopBig) files

$
0
0

Goal

Add PSB Search filter for searching PSB (PhotoShop Big) files with mime type application/vnd.3gpp.pic-bw-small by selecting File Type > Images > Bitmaps > Adobe Photoshop Big

For a predicate evaluator to search both PSB and PSD files using otb Adobe Photoshop filter check this post




Product




Extension




Solution

Create a servlet filter apps.experienceaem.assets.OmniSearchFileTypeOptionsFilter with sling.filter.resourceTypes set to dam/gui/coral/components/admin/customsearch/omnisearchpredicates/filetypepredicate/nestedcheckboxlist for adding a transient psb filter node 


package apps.experienceaem.assets;

import com.adobe.granite.ui.components.ds.AbstractDataSource;
import com.adobe.granite.ui.components.ds.DataSource;
import com.day.cq.commons.jcr.JcrUtil;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.framework.Constants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import javax.servlet.*;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM - Omnisearch file type options filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.resourceTypes=dam/gui/coral/components/admin/customsearch/omnisearchpredicates/filetypepredicate/nestedcheckboxlist"
}
)
public class OmniSearchFileTypeOptionsFilter implements Filter {
private static Logger log = LoggerFactory.getLogger(OmniSearchFileTypeOptionsFilter.class);

private static final String BITMAP_ROOT = "/libs/dam/content/predicates/omnisearch/mimetypes/items/images/sublist/items/bitmap/sublist";

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

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

String resourcePath = slingRequest.getResource().getPath();

if(!BITMAP_ROOT.equals(resourcePath)){
chain.doFilter(slingRequest, response);
return;
}

createTransientPSBFilterNode(slingRequest.getResourceResolver());

chain.doFilter(slingRequest, response);
}

private void createTransientPSBFilterNode(ResourceResolver resolver){
Node psbItem = null;

try{
Resource bitMapItemsResource = resolver.getResource(BITMAP_ROOT).getChild("items");
Resource psResource = resolver.getResource(bitMapItemsResource.getPath() + "/photoshop");
Session session = resolver.adaptTo(Session.class);

psbItem = JcrUtil.createPath(bitMapItemsResource.getPath() + "/psb", "nt:unstructured", "nt:unstructured", session, false);

psbItem.setProperty("text", "Adobe Photoshop PSB");
psbItem.setProperty("value", "application/vnd.3gpp.pic-bw-small");

bitMapItemsResource.adaptTo(Node.class).orderBefore(psbItem.getName(), psResource.getName());
}catch(Exception e){
log.error("Error creating transient PSB filter node", e);
}
}

@Override
public void destroy() {
}
}


AEM 6560 - React SPA Dynamic Media Smart Crop Image Component

$
0
0

Goal

Create a React SPA Smart Crop Image component /apps/eaem-sites-spa-how-to-react/components/dm-image-smart-crop to show the dynamic crops for different breakpoints...

Demo | Package Install | Github


Setup DM Image Profiles


Configure Folder with DM Info


Smart Crops


Component Dialog


Desktop


Mobile



Solution

1) To get the smart crops of an image,  create the following nt:file /apps/eaem-sites-spa-how-to-react/smart-crop-renditions/smart-crop-renditions.jsp to return the dynamic crops as JSON (serves as a client side datasource for crops drop down, created in next steps...)


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

<%
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("url", dynVM.get("copyurl"));

dynRenditions.put(name, dynRendition);
}

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


2) Create /apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions and set the sling:resourceType to /apps/eaem-sites-spa-how-to-react/smart-crop-renditions. This is the content node for fetching renditions...

                                                        http://localhost:4502/apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions.html/content/dam/home-assets/chaitra-birthday.JPG

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

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

                                                        dm-smart-crops.js

5) Create file (nt:file) /apps/eaem-sites-spa-how-to-react/clientlibs/clientlib-extensions/dm-smart-crops.js, add the following code...

(function ($, $document) {
var DM_FILE_REF = "[name='./fileReference']",
CROPS_MF = "[data-granite-coral-multifield-name='./crops']",
SMART_CROPS_URL = "/apps/eaem-sites-spa-how-to-react/smart-crop-renditions/renditions.html",
dynRenditions = {};

$document.on('dialog-ready', loadSmartCrops);

function loadSmartCrops() {
var dialogPath;

try {
dialogPath = Granite.author.DialogFrame.currentDialog.editable.slingPath;
} catch (err) {
console.log("Error getting dialog path...", err);
}

if (!dialogPath) {
return;
}

dialogPath = dialogPath.substring(0, dialogPath.lastIndexOf(".json"));

$.ajax(dialogPath + ".2.json").done(handleCropsMF);
}

function handleCropsMF(dialogData) {
var $cropsMF = $(CROPS_MF),
mfName = $cropsMF.attr("data-granite-coral-multifield-name"),
selectData = dialogData[mfName.substr(2)];

$cropsMF.find("coral-select").each(function (index, cropSelect) {
var $cropSelect = $(cropSelect), selUrl,
name = $cropSelect.attr("name");

name = name.substring(mfName.length + 1);
name = name.substring(0,name.indexOf("/"));

if(selectData[name]){
selUrl = selectData[name]["url"];
}

loadCropsInSelect($cropSelect, selUrl);
});

$cropsMF.on("change", function () {
var multifield = this;

_.defer(function () {
var justAddedItem = multifield.items.last(),
$cropSelect = $(justAddedItem).find("coral-select");

loadCropsInSelect($cropSelect);
});
});

$(DM_FILE_REF).closest("coral-fileupload").on("change", function(){
dynRenditions = {};

$cropsMF.find("coral-select").each(function (index, cropSelect) {
var $cropSelect = $(cropSelect);

$cropSelect[0].items.clear();

loadCropsInSelect($cropSelect);
});
})
}

function getCoralSelectItem(text, value, selected) {
return '<coral-select-item value="' + value + '"' + selected + '>' + text + '</coral-select-item>';
}

function loadCropsInSelect($cropSelect, selectedValue) {
var $fileRef = $(DM_FILE_REF),
fileRef = $fileRef.val();

if ( !fileRef || ($cropSelect[0].items.length > 1)) {
return;
}

if (_.isEmpty(dynRenditions)) {
$.ajax({url: SMART_CROPS_URL + fileRef, async: false}).done(function (renditions) {
dynRenditions = renditions;
addInSelect();
});
} else {
addInSelect();
}

function addInSelect() {
_.each(dynRenditions, function (rendition) {
$cropSelect.append(getCoralSelectItem(rendition.name, rendition.url,
((selectedValue == rendition.url) ? "selected" : "")));
});
}
}
}(jQuery, jQuery(document)));


6) Create the component /apps/eaem-sites-spa-how-to-react/components/dm-image-smart-crop. In the next step we'd be creating the react render type script...


7) Add the component render script in eaem-sites-react-spa-dm-image\ui.frontend\src\components\DMSmartCropImage\DMSmartCropImage.tsx with the following code...

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

function isObjectEmpty(obj) {
return (Object.keys(obj).length == 0);
}

interface ImageComponentProps {
smartCrops: object
fileReference: string
imageLink: string
}

interface ImageComponentState {
imageSrc: string
}

const ImageEditConfig = {
emptyLabel: 'Dynamic Media Smart Crop Image - Experience AEM',

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

class Image extends React.Component<ImageComponentProps, ImageComponentState> {
constructor(props: ImageComponentProps) {
super(props);

this.state = {
imageSrc: this.imageUrl()
}
}

componentDidMount() {
window.addEventListener('resize', this.updateImage.bind(this));
}

componentDidUpdate(){
console.log("in update");

const currentSrc = this.state.imageSrc;
const newSrc = this.imageUrl();

if(currentSrc != newSrc){
this.updateImage();
}
}

componentWillUnmount() {
window.removeEventListener('resize', this.updateImage);
}

updateImage(){
this.setState({
imageSrc: this.imageUrl()
})
}

imageUrl() {
const imageProps = this.props;
let src = imageProps.fileReference;

if (!isObjectEmpty(imageProps.smartCrops)) {
const breakPoints = Object.keys(imageProps.smartCrops).sort((a: any, b: any) => b - a);

for (const i in breakPoints) {
let bp = parseInt(breakPoints[i]);

if (bp < window.innerWidth) {
src = imageProps.smartCrops[bp];
break;
}
}
}

return src;
}

get imageHTML() {
const imgStyles: CSS.Properties = {
display : 'block',
marginLeft: 'auto',
marginRight: 'auto'
};

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

render() {
return this.imageHTML;
}
}

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


8) DMSmartCropImage was imported in ui.frontend\src\components\import-components.js

                           ...

                           import './DMSmartCropImage/DMSmartCropImage';

9) Create a Sling Model Exporter com.eaem.core.models.ImageComponentSlingExporter for exporting the component properties

                          SPA App Model Export: 

                          http://localhost:4502/content/eaem-sites-spa-how-to-react/us/en.model.json

                          Container Component Model Export: 

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


10) Add the following code in com.eaem.core.models.ImageComponentSlingExporter

package com.eaem.core.models;

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

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

@Model(
adaptables = {SlingHttpServletRequest.class},
adapters = {ComponentExporter.class},
resourceType = {
"eaem-sites-spa-how-to-react/components/image",
"eaem-sites-spa-how-to-react/components/dm-image-smart-crop"
}
)
@Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class ImageComponentSlingExporter implements ComponentExporter {

@Inject
private Resource resource;

@ValueMapValue
@Optional
private String imageLink;

@ValueMapValue
@Optional
private String fileReference;

@ValueMapValue
@Optional
private boolean openInNewWindow;

private Map<String, String> smartCrops;

@PostConstruct
protected void initModel() {
smartCrops = new LinkedHashMap<String, String>();

Resource cropsRes = resource.getChild("crops");

if(cropsRes == null){
return;
}

Iterator<Resource> itr = cropsRes.listChildren();
ValueMap vm = null;

while(itr.hasNext()){
vm = itr.next().getValueMap();
smartCrops.put(vm.get("breakpoint", ""), vm.get("url", ""));
}
}

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

public Map<String, String> getSmartCrops() {
return smartCrops;
}

public String getImageLink() {
return imageLink;
}

public void setImageLink(String imageLink) {
this.imageLink = imageLink;
}

public String getFileReference() {
return fileReference;
}

public void setFileReference(String fileReference) {
this.fileReference = fileReference;
}

public boolean isOpenInNewWindow() {
return openInNewWindow;
}

public void setOpenInNewWindow(boolean openInNewWindow) {
this.openInNewWindow = openInNewWindow;
}
}


AEM 6560 - Sites Editor Asset Finder set Default Folder

$
0
0

Goal

Sites pages loaded for editing show the assets in /content/dam root by default. This post is for setting the path to a specific project folder...

Demo | Package Install | Github




Solution

1) Add a client library to add editor hook. Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-asset-finder-default-folder


2) Create node /apps/eaem-asset-finder-default-folder/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.editor.hook.assetfinder], String[] property dependencies with value lodash.

3) Create file (nt:file) /apps/eaem-asset-finder-default-folder/clientlib/js.txt, add

                        default-folder.js

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

(function ($, $document, author) {
var self = {},
IMAGES_FINDER = "Images",
PATH_FIELD = "foundation-autocomplete[name='assetfilter_image_path']",
DEFAULT_FOLDER = '/content/dam/we-retail';

var searchPath = self.searchRoot,
imageServlet = '/bin/wcm/contentfinder/asset/view.html',
itemResourceType = 'cq/gui/components/authoring/assetfinder/asset';

self.loadAssets = function (query, lowerLimit, upperLimit) {
if(_.isEmpty(searchPath)){
searchPath = DEFAULT_FOLDER;
$(PATH_FIELD)[0]._input.value = DEFAULT_FOLDER;
}

var param = {
'_dc': new Date().getTime(),
'query': query.concat("order:\"-jcr:content/jcr:lastModified\""),
'mimeType': 'image',
'itemResourceType': itemResourceType,
'limit': lowerLimit + ".." + upperLimit,
'_charset_': 'utf-8'
};

return $.ajax({
type: 'GET',
dataType: 'html',
url: Granite.HTTP.externalize(imageServlet) + searchPath,
data: param
});
};

self.setSearchPath = function (spath) {
searchPath = spath;
};

author.ui.assetFinder.register(IMAGES_FINDER, self);
}(jQuery, jQuery(document), Granite.author));

AEM 6560 - React SPA component for showing Alert Banner using Material UI

$
0
0

Goal

Add AEM SPA React Alert component showing a banner created using Material UI (MUI) at the top of page. Using MUI there are no global style-sheets, each component is independent and there are no css conflicts at page level. Responsive page layout is created using AEM Grid...

Thank you Joe Ritchie and team...

Demo | Package Install | Github


Banner (View as Published)



Component Dialog in Template



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) Open a command prompt (terminal) at eaem-sites-react-spa-material-ui-banner\ui.frontend and install the typscript and material ui specific dependencies

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

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



5) Add the component render script in eaemeaem-sites-react-spa-material-ui-banner\ui.frontend\src\components\Alert\EAEMAlert.tsx with the following code...

import { MapTo } from "@adobe/cq-react-editable-components";
import React, { FC, useState, useEffect } from "react";
import { Link } from "react-router-dom";
import {
IconButton,
Typography,
createStyles,
makeStyles,
Theme,
Portal,
SvgIcon,
SvgIconProps,
Collapse
} from "@material-ui/core";
import CSS from "csstype";
import classNames from "classnames";

const iconStyles = makeStyles(() =>
createStyles({
root: {
fontSize: 20
}
})
);

const EAEMCloseIcon: FC<SvgIconProps> = props => {
const classes = iconStyles();

return (
<SvgIcon
viewBox="0 0 20 20"
{...props}
className={classNames(classes.root, props.className)}
>
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<g
id="Symbols"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g id="Grommet/X-Close" transform="translate(-15.000000, -15.000000)">
<rect id="Rectangle" x="0" y="0" width="50" height="50"></rect>
<path
d="M34.3548387,15 L35,15.6451613 L25.645,24.999 L35,34.3548387 L34.3548387,35 L25,25.645 L15.6451613,35 L15,34.3548387 L24.354,25 L15,15.6451613 L15.6451613,15 L25,24.354 L34.3548387,15 Z"
id="Combined-Shape"
fill="currentColor"
></path>
</g>
</g>
</SvgIcon>
);
};

type AlertProps = {
showAlert: string;
text: string;
linkURL: string;
};

const AlertEditConfig = {
emptyLabel: "Alert - Shows banner at the top of page",

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

const useStyles = makeStyles((theme: Theme) =>
createStyles({
closeIcon: {
fontSize: 12,
color: "white",
[theme.breakpoints.up("sm")]: {
fontSize: 16
}
},
container: {
alignItems: "center",
background: "black",
display: "flex"
},
content: {
color: "white",
paddingTop: "15px",
paddingBottom: "15px",
flex: "1 1",
fontFamily: 'Times, serif',
fontSize: 16,
textAlign: "center"
}
})
);

const EAEMAlert: FC<AlertProps> = props => {
const classes = useStyles();

const [open, setOpen] = useState(true);
const [root, setRoot] = useState<HTMLElement | null>(null);

useEffect(() => {
setRoot(document.getElementById("eaem-alert-banner"));
}, [root]);

const handleClose = () => {
setOpen(false);
};

if (!props.text || props.showAlert != "true") {
return null;
}

let text = props.text.trim();

if (text.startsWith("<p>") && text.endsWith("</p>")) {
text = text.substring(3, text.lastIndexOf("</p>"));
}

return (
<Portal container={root}>
<Collapse in={open}>
<Typography className={classes.container} component={"div"}>
<div
className={classes.content}
dangerouslySetInnerHTML={{ __html: text }}
/>
<IconButton onClick={handleClose}>
<EAEMCloseIcon className={classes.closeIcon} />
</IconButton>
</Typography>
</Collapse>
</Portal>
);
};

export default MapTo("eaem-sites-spa-how-to-react/components/alert")(EAEMAlert, AlertEditConfig);

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

                                                          import './Page/Page';
                                                          import './Text/Text';
                                                          import './Title/Title';
                                                          import './Nav/Nav';
                                                          import './Image/Image';
                                                          import './Alert/EAEMAlert';


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

$
0
0

Goal

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

Package Install | Github



Solution


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

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

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

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

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

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

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

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

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

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

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

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

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

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

const eaemTheme = createMuiTheme({
breakpoints: {
values: {
xs: BREAKPOINTS.XS,
sm: BREAKPOINTS.SM,
md: BREAKPOINTS.MD,
lg: BREAKPOINTS.LG,
xl: BREAKPOINTS.XL
}
}
});

const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
fontFamily: 'AdobeCaslonPro, Times, serif !important',
'& h1': {
[theme.breakpoints.down("xl")]: {
fontSize: '34px',
},
[theme.breakpoints.down("lg")]: {
fontSize: '30px',
},
[theme.breakpoints.down("md")]: {
fontSize: '26px',
}
},
'& h2': {
[theme.breakpoints.down("xl")]: {
fontSize: '28px',
},
[theme.breakpoints.down("lg")]: {
fontSize: '25px',
},
[theme.breakpoints.down("md")]: {
fontSize: '22px',
}
},
'& h3': {
[theme.breakpoints.down("xl")]: {
fontSize: '22px',
},
[theme.breakpoints.down("lg")]: {
fontSize: '20px',
},
[theme.breakpoints.down("md")]: {
fontSize: '18px',
}
},
'& p': {
fontSize: '13px',
}
}
})
);

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

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

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

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

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

AEM Cloud Manager - Setup Git Remotes and Authentication on Windows

$
0
0


The Initial Sync section of Integrating with Git page explains setting up two remotes (origin for customer specific remote and adobe for cloud manager specific remote). This simple post is on providing authentication credentials in git bash so you can fetch both remotes...


1) You set the cloud manager remote by adding, for eg.


                                        git remote add adobe https://git.cloudmanager.adobe.com/demosystem/experience-aem-demo-cs


2) Try fetching the remotes using git fetch --all and you might see the error fatal: Authentication failed for 'https://git.cloudmanager.adobe.com/demosystem/experience-aem-demo-cs'



3) To get around the issue, set Windows git credential helper


                                        git config --global credential.helper wincred


4) Add credential config for the repo....


                                        git config credential.https://git.cloudmanager.adobe.com/demosystem/experience-aem-demo-cs.username your-user-name


5) It adds an entry in repo config file experience-aem-demo-cs\.git\config


6) Trying a fetch now shows authentication window...



7) Successful fetch...







AEM 6560 - SPA Editor Container Class as React SPA Functional Component

$
0
0

Goal

React hooks are not allowed in class components. This post is for using hooks in AEM Editable Component Container coded as a Class. In the following example we use Material UI useMediaQuery hook in a AEM SPA Editable Component for setting breakpoint specific background height...


Container on Desktop


Container on Mobile



Component TypeScript


import React, { FC, useState, useEffect, Component, ComponentType } from "react";
import CSS from 'csstype';
import { MapTo, Container } from "@adobe/cq-react-editable-components";
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { useTheme, createMuiTheme } from '@material-ui/core/styles';

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

const eaemTheme = createMuiTheme({
breakpoints: {
values: {
xs: BREAKPOINT.XS,
sm: BREAKPOINT.SM,
md: BREAKPOINT.MD,
lg: BREAKPOINT.LG,
xl: BREAKPOINT.XL
}
}
});

interface EAEMContainerPropTypes {
containerProps: any,
childComponents: any,
placeholderComponent: any
}

const EAEMContainerWrapper = (Component: React.FC<any>) => class EAEMContainer extends Container<any, any> {
props: any

constructor(props: any) {
super(props);
this.props = props;
}

render() {
return (
<Component {...Object.assign({}, this.props, {
containerProps: super.containerProps,
childComponents: super.childComponents,
placeholderComponent: super.placeholderComponent
})}>
</Component>
);
}
}

const EAEMPositioningContainer: FC<EAEMContainerPropTypes> = ({ containerProps, childComponents, placeholderComponent, ...props }) => {
let height = "500px";

//cannot use conditional if-else statements with React hooks
let matches = useMediaQuery(eaemTheme.breakpoints.down('md'));

if (matches) {
height = "300px";
}

matches = useMediaQuery(eaemTheme.breakpoints.down('sm'));

if (matches) {
height = "100px";
}

containerProps.style = {
height: height,
"background-color" : "yellow"
}

return (
<div {...containerProps}>
{childComponents}
{placeholderComponent}
</div>
);
};

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



AEM 6560 - Sites show author entered component name in content tree

$
0
0


When multiple components of the same type eg. Text are added on a page, it might be confusing finding the right component when author uses content tree. The following extension gives flexibility adding more user friendly component instance name on page...

Demo | Package Install | Github


Product Content Tree


Extended Content Tree




Solution

1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-content-tree-comp-name

2) For the Additional Info dialog tab html, create node /apps/eaem-content-tree-comp-name/ui/addInfo with the following structure

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="EAEM Additional Info"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
granite:class="cq-RichText-FixedColumn-column"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<authoringName
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Author given component name..."
fieldLabel="Authoring name"
name="./authoringName"/>
</items>
</column>
</items>
</columns>
</items>
</jcr:root>

3) Create node /apps/eaem-content-tree-comp-name/clientlib of type cq:ClientLibraryFolder, add String[] property categories with value [cq.authoring.dialog.all], String[] property dependencies with value lodash.

4) Create file (nt:file) /apps/eaem-content-tree-comp-name/clientlib/js.txt, add

                        custom-comp-names.js

5) Create file (nt:file) /apps/eaem-content-tree-comp-name/clientlib/custom-comp-names.js, add the following code

(function($, $document){
var CORAL_DIALOG_SEL = "coral-dialog.is-open",
EAEM_ADDN_INFO_TAB = "dialog-eaem-addn-info",
ADDN_INFO_DIALOG_URL = "/apps/eaem-content-tree-comp-name/ui/addInfo.html",
ADDN_INFO_HTML, AUTHORING_NAME_FIELD = "authoringName",
authoringNames = {};

$document.on("coral-overlay:open", "coral-dialog", addComponentAdditionalInfoTab);

$document.on("cq-editables-updated", $.debounce(500, false, fetchAuthoringNames));

$document.on("cq-editor-loaded", extendContentTree);

function fetchAuthoringNames(){
var contentTree = Granite.author.ui.contentTree;

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

var responsiveGridPath = contentTree.editables[0].path;

$.ajax( { url: responsiveGridPath + ".infinity.json", async: false } ).done(function(data){
authoringNames = loadComponentAuthoringNames(responsiveGridPath, data, {});
});
}

function extendContentTree(){
fetchAuthoringNames();

var contentTree = Granite.author.ui.contentTree,
_orignFn = contentTree._getElementTitle;

contentTree._getElementTitle = function (editable, componentTitle) {
var titleHtml = _orignFn.call(this, editable, componentTitle),
authoringTitle = authoringNames[editable.path];

if(authoringTitle){
titleHtml = "<span class='editor-ContentTree-itemTitle'>" + authoringTitle + "</span>";
}

return titleHtml;
}
}

function loadComponentAuthoringNames(path, data, authoringNames){
_.each(data, function(value, nodeName){
if(_.isObject(value)){
loadComponentAuthoringNames(path + "/" + nodeName, value, authoringNames);
}

if( (nodeName === AUTHORING_NAME_FIELD) && value){
authoringNames[path] = value;
}
});

return authoringNames;
}

function loadAddnInfoHtml(){
$.ajax(ADDN_INFO_DIALOG_URL).done(function(html){
ADDN_INFO_HTML = html;
})
}

function addComponentAdditionalInfoTab(){
var $dialog = $(CORAL_DIALOG_SEL);

if(($dialog.length == 0) || ($("#" + EAEM_ADDN_INFO_TAB).length > 0) || !ADDN_INFO_HTML){
return;
}

var $panelTabs = $dialog.find("coral-tabview");

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

$panelTabs[0].tabList.items.add({
label: {
innerHTML: '<span id="' + EAEM_ADDN_INFO_TAB + '">EAEM Additional Info</span>'
}
});

var panelStack = $panelTabs[0].panelStack;

panelStack.items.add({
content: {
innerHTML: ADDN_INFO_HTML
}
});

loadAddInfoContent($dialog);
}

function loadAddInfoContent($dialog){
var dialogPath;

try {
dialogPath = Granite.author.DialogFrame.currentDialog.editable.slingPath;
} catch (err) {
console.log("Error getting dialog path...", err);
}

if (!dialogPath) {
return;
}

dialogPath = dialogPath.substring(0, dialogPath.lastIndexOf(".json"));

$.ajax(dialogPath + ".2.json").done(function(data){
$dialog.find("[name='./" + AUTHORING_NAME_FIELD + "']").val(data[AUTHORING_NAME_FIELD]);
});
}

loadAddnInfoHtml();
}(jQuery, jQuery(document)));


AEM Cloud Service - Validate asset file extensions while uploading to AEM in browser

$
0
0

Goal

The following steps explain adding a client side script to validate the file types while uploading to AEM using browser..

Demo | Package Install | Github




Solution

1) Create a project eaem-cs-validate-assets using archetype - https://github.com/adobe/aem-project-archetype

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype 
-D archetypeVersion=24 -D aemVersion=cloud -D appTitle="Experience AEM Validate Assets on Upload"
-D appId="eaem-cs-validate-assets" -D groupId="apps.experienceaem.assets" -D frontendModule=none
-D includeExamples=n -D includeDispatcherConfig=n

2) Create node /apps/eaem-cs-validate-assets/clientlibs/supported-file-types of type cq:ClientLibraryFolder, add String[] property categories with value [dam.gui.coral.fileupload], String[] property dependencies with value eaem.lodash.

3) Create file (nt:file) /apps/eaem-cs-validate-assets/clientlibs/supported-file-types/js.txt, add

                        supported-file-types.js

4) Create file (nt:file) /apps/eaem-cs-validate-assets/clientlibs/supported-file-types/supported-file-types.js, add the following code

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

var _ = window._,
ERROR_MSG = "Unsupported file extensions : ";

var _origConfirmUpload = window.DamFileUpload.prototype._confirmUpload;

window.DamFileUpload.prototype._confirmUpload = function (event) {
var invalidFileNames = [],
FILE_EXTS_SUPPORTED = getSuppportedFileExtensions();

this.fileUpload.uploadQueue.forEach(function(item) {
var fileName = item.name;

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

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

if(!FILE_EXTS_SUPPORTED.includes(ext.toUpperCase())){
invalidFileNames.push(fileName);
}
});

if(_.isEmpty(invalidFileNames)){
_origConfirmUpload.call(this, event);

var uploadDialog = this.uploadDialog;

_.defer(function(){
$(uploadDialog).find("input").attr("disabled", "disabled");
},0)
}else{
showAlert(ERROR_MSG + "<b>" + invalidFileNames.join(",") + "</b>");
}
};

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);
}

function getSuppportedFileExtensions(){
return [
"JPG", "PNG"
];
}
}(jQuery, jQuery(document)));


5) More supported file extensions can be added by modifying #55 function getSuppportedFileExtensions()

AEM Cloud Service - Unique assets check while uploading files to AEM in browser (avoid duplicate file names)

$
0
0


AEM Cloud Service Version : 2021.1.4738.20210107T143101Z-201217 (Jan 7, 2021)

Goal

Adding the following extension would restrict users from uploading duplicates files (files with same name). The check is done repository wide /content/dam 

Demo | Package Install | Github



Solution

1) Create a service user configuration in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-upload-unique-assets\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config this is for querying and returning duplicates as JSON (in next steps). 

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

2) Adding the above repo init script in code repo and installing on AEM CS creates a service user in /home/users/system/cq:services/experience-aem


3) Add the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-upload-unique-assets\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-eaem.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-cs-upload-unique-assets.core:eaem-service-user=[eaem-service-user]]"/>


4) Create a servlet apps.experienceaem.assets.core.servlets.FindAssetDuplicates to return the duplicates 

package apps.experienceaem.assets.core.servlets;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

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

@Reference
private ResourceResolverFactory factory;

@Reference
private QueryBuilder builder;

@Override
protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws
ServletException, IOException {
try {
String fileNames[] = request.getParameterValues("fileName");
JSONArray duplicates = new JSONArray();

response.setContentType("application/json");

if(ArrayUtils.isEmpty(fileNames)){
duplicates.write(response.getWriter());
return;
}

ResourceResolver resourceResolver = getServiceResourceResolver(factory);

Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap(fileNames)),
resourceResolver.adaptTo(Session.class));

SearchResult result = query.getResult();

for (Hit hit : result.getHits()) {
duplicates.put(hit.getPath());
}

duplicates.write(response.getWriter());
} catch (Exception e) {
log.error("Could not execute duplicates check", e);
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

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

private Map<String, String> getQueryPredicateMap(String[] fileNames) {
Map<String, String> map = new HashMap<>();
map.put("path", "/content/dam");
map.put("group.p.or", "true");

for(int index = 0; index < fileNames.length; index++){
map.put("group." + index + "_nodename", fileNames[index]);
}

return map;
}
}


5) Create a client library /apps/eaem-cs-upload-unique-assets/clientlibs/unique-names with categories =dam.gui.coral.fileupload and dependencies=eaem.lodash and following script

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

var _ = window._;

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

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

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

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

var validFiles = [];

_.each(files, function(file) {
validFiles.push(file.name);
});

var existInDAMFiles = checkFilesExist(validFiles);

if(!_.isEmpty(existInDAMFiles)){
showAlert("Following files exist : BR BR" + existInDAMFiles.join("BR") + "</b>");
            return;
}

_origOnInputChange.call(this, event);
};

window.DamFileUpload.prototype._confirmUpload = function (event) {
var existInDAMFiles = checkFilesExist(this.fileUpload.uploadQueue.map(hit => hit.path));

if(!_.isEmpty(existInDAMFiles)){
showAlert("Following files exist : BR BR" + existInDAMFiles.join("BR") + "</b>");
            return;
}

_origConfirmUpload.call(this, event);

var uploadDialog = this.uploadDialog;

_.defer(function(){
$(uploadDialog).find("input").attr("disabled", "disabled");
},0)
};

function checkFilesExist(fileNames){
var existingFiles = [],
url = "/bin/experience-aem/duplicates?";

_.each(fileNames, function(fileName, index){
url = url + "fileName=" + fileName + "&";
});

$.ajax( { url : url, async : false, contentType:"application/json" }).done(function(data){
existingFiles = data;
}).fail(function() {
showAlert("Error occured while checking for duplicates", "Error");
});

return existingFiles;
}

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

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

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


AEM Cloud Service - Dynamic Media Process New Smart Crops and Keep Existing

$
0
0


Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

When new smart crop settings are added in Image Profile, assets have to be reprocessed to see the new crops (otherwise they show unprocessed). However reprocessing results in generating all crops again, so any manual adjustments made to the previous crops by creatives get overwritten. This post explains how to preserve the manually adjusted smart crops, and generate new unprocessed ones...for scene7 api check this documentation

Demo | Package Install | Github


Process New Smart Crops


Unprocessed crops


Smart Crop Update Workflow



Solution

1) Add a datasource to read the existing smart crops /apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions

<%@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.Tag"%>
<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@ page import="apps.experienceaem.assets.core.services.DMCService" %>

<%
ValueMap dynVM = null;

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

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

DataSource rendsDS = null;

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

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

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

dynResource = items.next();

dynVM = dynResource.getValueMap();

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

dynRendition.put("name", name);
dynRendition.put("cropdata", getCropData(dynVM));

dynRenditions.put(name, dynRendition);
}

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

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

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

cropArray.put(cropData);

return cropArray;
}
%>


2) To get the smart crops as json send the asset path as resource suffix to data source eg. https://author-pxxxxx-exxxxx.adobeaemcloud.com/apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions/renditions.html/content/dam/experience-aem/am-i-doing-this-right.png


3) Code a servlet apps.experienceaem.assets.core.servlets.UpdateSmartCropSettings to read the existing smart crops and kickoff the smart crop update workflow (added in next steps), when the button Process New Smart Crops is clicked in Asset Details console...

package apps.experienceaem.assets.core.servlets;

import apps.experienceaem.assets.core.services.DMCService;
import com.day.cq.workflow.WorkflowService;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.model.WorkflowModel;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Iterator;

@Component(
name = "Experience AEM Update Smart Crop settings",
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.methods=POST",
"sling.servlet.paths=/bin/eaem/update-smart-crops"
}
)
public class UpdateSmartCropSettings extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(UpdateSmartCropSettings.class);

private static String SMART_CROPS_RES = "/apps/eaem-cs-process-new-smart-crops/extensions/smart-crop-renditions/renditions.html";
private static String CROP_DATA = "cropdata";
private static final String UPDATE_SMART_CROPS_WF_PATH = "/var/workflow/models/experience-aem-update-smart-crops";

@Reference
private DMCService dmcService;

@Reference
private WorkflowService workflowService;

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

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

RequestDispatcher dp = request.getRequestDispatcher(SMART_CROPS_RES + path);

SlingHttpServletResponse wrapperResponse = new DefaultSlingModelResponseWrapper(response);

dp.include(request, wrapperResponse);

String smartCropsStr = wrapperResponse.toString();

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

JSONObject smartCrops = new JSONObject(smartCropsStr);
JSONArray smartCropsToUpdate = new JSONArray();

Iterator smartCropKeys = smartCrops.keys();
JSONObject smartCrop = null;

while(smartCropKeys.hasNext()){
smartCrop = (JSONObject)smartCrops.get(String.valueOf(smartCropKeys.next()));

if(!smartCrop.has(CROP_DATA)){
continue;
}

JSONObject currentCrop = (JSONObject)(((JSONArray)smartCrop.get(CROP_DATA)).get(0));

if(StringUtils.isEmpty((String)currentCrop.get("id"))){
continue;
}

smartCropsToUpdate.put(currentCrop);
}

ResourceResolver resolver = request.getResourceResolver();
WorkflowSession wfSession = workflowService.getWorkflowSession(resolver.adaptTo(Session.class));

WorkflowModel wfModel = wfSession.getModel(UPDATE_SMART_CROPS_WF_PATH);
WorkflowData wfData = wfSession.newWorkflowData("JCR_PATH", path);
wfData.getMetaDataMap().put(DMCService.SMART_CROPS_JSON, smartCropsToUpdate.toString());

wfSession.startWorkflow(wfModel, wfData);

smartCropsToUpdate.write(response.getWriter());
} catch (Exception e) {
log.error("Could not get the smart crop settings", e);
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

private class DefaultSlingModelResponseWrapper extends SlingHttpServletResponseWrapper {
private CharArrayWriter writer;

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

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

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


4) Add the workflow process step apps.experienceaem.assets.core.services.UpdateSmartCropsProcess to update smart crops (restore to previous crop rect). The first step in workflow Scene7:Reprocess Assets generates all crops afresh (including the new unprocessed) and this step updates existing ones to their previous crop settings (assuming they have been manually adjusted....)

package apps.experienceaem.assets.core.services;

import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowData;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.commons.json.JSONArray;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
service = WorkflowProcess.class,
property = { "process.label=Update Smart Crops Workflow Process Step" })
public class UpdateSmartCropsProcess implements WorkflowProcess {

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

@Reference
private DMCService dmcService;

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

try{
MetaDataMap wfData = workItem.getWorkflow().getMetaDataMap();

log.info("Updating smart crops for asset : " + assetPath);

String smartCropsToUpdateStr = wfData.get(DMCService.SMART_CROPS_JSON, "");

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

log.info("Smart crops to update " + smartCropsToUpdateStr);

dmcService.updateSmartCropsInS7(assetPath, new JSONArray(smartCropsToUpdateStr));
}catch(Exception e){
log.error("Error occured while updating crops for payload - " + assetPath, e);
}
}

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

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

return payloadPath;
}
}


5) Add a helper service class apps.experienceaem.assets.core.services.impl.DMCServiceImpl for the Scene7 API...

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

import apps.experienceaem.assets.core.services.DMCService;
import com.adobe.granite.crypto.CryptoSupport;
import com.adobe.granite.license.ProductInfo;
import com.adobe.granite.license.ProductInfoService;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.scene7.api.*;
import com.scene7.ipsapi.*;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.sling.api.resource.*;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

@Component(service = DMCService.class)
public class DMCServiceImpl implements DMCService {
private static final Logger log = LoggerFactory.getLogger(DMCServiceImpl.class);

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

@Reference
private ResourceResolverFactory resourceResolverFactory;

@Reference
private Scene7Service scene7Service;

@Reference
private S7ConfigResolver s7ConfigResolver;

@Reference
private Scene7APIClient scene7APIClient;

@Reference
private ProductInfoService productInfoService;

@Reference
private CryptoSupport cryptoSupport;

@Reference
private Scene7EndpointsManager scene7EndpointsManager;

public Map<String, String> getSmartCropsSubAssetHandles(S7Config s7Config, String assetHandle){
Map<String, String> subAssetHandles = new HashMap<String, String>();

try{
GetAssociatedAssetsParam getAssociatedAssetsParam = new GetAssociatedAssetsParam();
getAssociatedAssetsParam.setCompanyHandle(s7Config.getCompanyHandle());
getAssociatedAssetsParam.setAssetHandle(assetHandle);

String responseBody = makeS7Request(s7Config, getAssociatedAssetsParam);

subAssetHandles = parseResponseForSubAssetHandles(responseBody.getBytes());
}catch(Exception e){
log.error("Error getting smart crop handles for : " + assetHandle, e);
}

return subAssetHandles;
}

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

public void updateSmartCropsInS7(String assetPath, JSONArray cropsToUpdate){
final ResourceResolver s7ConfigResourceResolver = getServiceResourceResolver(resourceResolverFactory);
S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, assetPath);

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

if((cropsToUpdate == null) || (cropsToUpdate.length() == 0)){
log.info("No crops to update for asset : " + assetPath);
return;
}

try{
JSONObject smartCrop = cropsToUpdate.getJSONObject(0);
String id = null, ownerHandle, subAssetHandle;

id = smartCrop.getString("id");
ownerHandle = id.substring(0, id.lastIndexOf("__")).replace("_", "|");

Map<String, String> subAssetHandles = getSmartCropsSubAssetHandles(s7Config, ownerHandle);

log.debug("subAssetHandles - " + subAssetHandles);

UpdateSmartCropsParam updateSmartCropsParam = new UpdateSmartCropsParam();
updateSmartCropsParam.setCompanyHandle(s7Config.getCompanyHandle());

SmartCropUpdateArray updateArray = new SmartCropUpdateArray();
SmartCropUpdate smartCropUpdate = null;
NormalizedCropRect cropRect = null;

double leftN, topN;

for(int i = 0; i < cropsToUpdate.length(); i++){
smartCrop = cropsToUpdate.getJSONObject(i);

smartCropUpdate = new SmartCropUpdate();
id = smartCrop.getString("id");

ownerHandle = id.substring(0, id.lastIndexOf("__")).replace("_", "|");
subAssetHandle = subAssetHandles.get(smartCrop.getString("name"));

log.debug("subAssetHandle - " + subAssetHandle + ", for name : " + smartCrop.getString("name"));

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

smartCropUpdate.setOwnerHandle(ownerHandle);
smartCropUpdate.setSubAssetHandle(subAssetHandle);

cropRect = new NormalizedCropRect();

leftN = Double.parseDouble(smartCrop.getString("leftN")) / 100;
topN = Double.parseDouble(smartCrop.getString("topN")) / 100;

cropRect.setLeftN(leftN);
cropRect.setTopN(topN);
cropRect.setWidthN(1 - (Double.parseDouble(smartCrop.getString("rightN")) / 100) - leftN);
cropRect.setHeightN(1 - (Double.parseDouble(smartCrop.getString("bottomN")) / 100) - topN);

smartCropUpdate.setCropRect(cropRect);

updateArray.getItems().add(smartCropUpdate);
}

updateSmartCropsParam.setUpdateArray(updateArray);

makeS7Request(s7Config, updateSmartCropsParam);
}catch(Exception e){
log.error("Error updating smart crops for : " + assetPath, e);
}
}

private String makeS7Request(S7Config s7Config, Object param) throws Exception{
AuthHeader authHeader = getS7AuthHeader(s7Config);
Marshaller marshaller = getMarshaller(AuthHeader.class);
StringWriter sw = new StringWriter();
marshaller.marshal(authHeader, sw);
String authHeaderStr = sw.toString();

marshaller = getMarshaller(param.getClass());
sw = new StringWriter();
marshaller.marshal(param, sw);
String apiMethod = sw.toString();

StringBuilder requestBody = new StringBuilder("<Request xmlns=\"http://www.scene7.com/IpsApi/xsd/2017-10-29-beta\">");
requestBody.append(authHeaderStr).append(apiMethod).append("</Request>");

String uri = scene7EndpointsManager.getAPIServer(s7Config.getRegion()).toString() + "/scene7/api/IpsApiService";
CloseableHttpClient client = null;
String responseBody = "";

SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build();
client = HttpClients.custom().setDefaultSocketConfig(sc).build();

HttpPost post = new HttpPost(uri);
StringEntity entity = new StringEntity(requestBody.toString(), "UTF-8");

post.addHeader("Content-Type", "text/xml");
post.setEntity(entity);

HttpResponse response = client.execute(post);

HttpEntity responseEntity = response.getEntity();

responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8");

log.info("Scene7 response - " + responseBody);

return responseBody;
}

private static Map<String, String> parseResponseForSubAssetHandles(byte[] responseBody) throws Exception{
Map<String, String> subAssetHandles = new HashMap<String, String>();

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

ByteArrayInputStream input = new ByteArrayInputStream(responseBody);

Document doc = builder.parse(input);

XPath xPath = XPathFactory.newInstance().newXPath();

String expression = "/getAssociatedAssetsReturn/subAssetArray/items";

NodeList itemList = (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET);
String subAssetHandle = null, name;

for (int i = 0; i < itemList.getLength(); i++) {
Node item = itemList.item(i);

if(item.getNodeType() == Node.ELEMENT_NODE) {
Element eElement = (Element) item;

subAssetHandle = eElement.getElementsByTagName("subAssetHandle").item(0).getTextContent();
name = eElement.getElementsByTagName("name").item(0).getTextContent();

subAssetHandles.put(name, subAssetHandle);
}
}

return subAssetHandles;
}

private AuthHeader getS7AuthHeader(S7Config s7Config) throws Exception{
ProductInfo[] prodInfo = productInfoService.getInfos();
String password = cryptoSupport.unprotect(s7Config.getPassword());

AuthHeader authHeader = new AuthHeader();
authHeader.setUser(s7Config.getEmail());
authHeader.setPassword(password);
authHeader.setAppName(prodInfo[0].getName());
authHeader.setAppVersion(prodInfo[0].getVersion().toString());
authHeader.setFaultHttpStatusCode(200);

return authHeader;
}

private Marshaller getMarshaller(Class apiMethodClass) throws JAXBException {
Marshaller marshaller = JAXBContext.newInstance(new Class[]{apiMethodClass}).createMarshaller();
marshaller.setProperty("jaxb.formatted.output", Boolean.valueOf(true));
marshaller.setProperty("jaxb.fragment", Boolean.valueOf(true));
return marshaller;
}
}


6) Create the workflow /conf/global/settings/workflow/models/experience-aem-update-smart-crops



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

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


8) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-process-new-smart-crops\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-cs-process-new-smart-crops.core:eaem-service-user=[eaem-service-user]]"/>


9) For adding the button Process New Smart Crops in Asset details action bar, create a clientlib /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/clientlib with categories dam.gui.actions.coral


10) Create the button configuration /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/content/process-smart-crops-but

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/collection/action"
icon="refresh"
target=".cq-damadmin-admin-childpages"
text="Process New Smart Crops"
variant="actionBar"/>


11) Add the button JS logic in /apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/clientlib/process-smart-crops.js 

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

var ASSET_DETAILS_PAGE = "/assetdetails.html",
initialized = false,
REPROCESS_ACTIVATOR = "dam-asset-reprocessassets-action-activator",
BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-download-activator",
PROCESS_NEW_SMART_CROPS_ACT_URL = "/bin/eaem/update-smart-crops?path=",
PROCESS_NEW_SMART_CROPS_BUT_URL = "/apps/eaem-cs-process-new-smart-crops/clientlibs/update-smart-crops/content/process-smart-crops-but.html";

if (!isAssetDetailsPage()) {
return;
}

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

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

initialized = true;

if(!getAssetMimeType().startsWith("image/")){
return;
}

$.ajax(PROCESS_NEW_SMART_CROPS_BUT_URL).done(addProcessNewSmartCropsButton);
}

function getAssetMimeType(){
return $("#image-preview").data("assetMimetype") || "";
}

function addProcessNewSmartCropsButton(html) {
var $eActivator = $("." + REPROCESS_ACTIVATOR);

if ($eActivator.length == 0) {
$eActivator = $("." + BESIDE_ACTIVATOR);
}

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

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

function updateSmartCrops() {
var assetUrl = window.location.pathname.substring(ASSET_DETAILS_PAGE.length);

$.ajax({url: PROCESS_NEW_SMART_CROPS_ACT_URL + assetUrl});

showAlert("Processing new smart crops...", "Smart Crop", "Default");
}

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

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

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

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


AEM Cloud Service - Dynamic Media folder path length validation, prevent deep nesting

$
0
0

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

When the Scene7 sync path (including account name) is more than 255 chars, asset upload to AEM works fine, however sync to Scene7 fails with error in s7 job console Failed to insert to DB. This post is on providing folder path validation (240 chars) so users do not upload assets in a deep nested structure and corrupt the repo with DM sync failures...

Demo | Package Install | Github


Upload Validation in Browser


Upload Validation in Desktop App


S7 Job Console Error for Path > 255 chars


Solution

1) Create a filter apps.experienceaem.assets.core.filters.FolderNestingCheck, to provide server side validation of folder path lengths...

package apps.experienceaem.assets.core.filters;

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

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

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM folder nesting check",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=((.*.initiateUpload.json)|(.*.createasset.html))",
}
)
public class FolderNestingCheck implements Filter {
private static Logger log = LoggerFactory.getLogger(FolderNestingCheck.class);

private static int S7_FOLDER_LENGTH_MAX = 240;
private static String CONTENT_DAM_PATH = "/content/dam";
private static String INITIATE_UPLOAD_JSON = ".initiateUpload.json";
private static String CREATE_ASSET_HTML = ".createasset.html";

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

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

String uri = slingRequest.getRequestURI();

if(!uri.endsWith(INITIATE_UPLOAD_JSON) && !uri.endsWith(CREATE_ASSET_HTML)){
chain.doFilter(request, response);
return;
}

if(uri.contains(CONTENT_DAM_PATH)){
String folderPath = uri.substring(uri.indexOf(CONTENT_DAM_PATH) + CONTENT_DAM_PATH.length());

if(folderPath.endsWith(INITIATE_UPLOAD_JSON)){
folderPath = folderPath.substring(0, folderPath.lastIndexOf(INITIATE_UPLOAD_JSON));
}else if(folderPath.endsWith(CREATE_ASSET_HTML)){
folderPath = folderPath.substring(0, folderPath.lastIndexOf(CREATE_ASSET_HTML));
}

if(folderPath.length() > S7_FOLDER_LENGTH_MAX){
log.info("Uploading to deep nested folders not allowed : " + uri);
slingResponse.sendError(SlingHttpServletResponse.SC_FORBIDDEN, "Uploading to deep nested folders not allowed: " + uri);
return;
}
}

chain.doFilter(request, response);
}catch(Exception e){
log.error("Error checking folder nesting", e);
}
}

@Override
public void destroy() {
}
}

2) For showing the validation alert in browser, create a clientlib /apps/eaem-cs-no-deep-nesting/clientlibs/no-deep-nesting with categories dam.gui.coral.fileupload

3) Add JS logic for validation in /apps/eaem-cs-no-deep-nesting/clientlibs/no-deep-nesting/deep-nesting-upload.js

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

var _ = window._,
S7_FOLDER_LENGTH_MAX = 240,
CONTENT_DAM_PATH = "/content/dam";

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

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

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

if(!files || (files.length == 0)){
_origOnInputChange.call(this, event);
return;
}

if(!isS7RelativeFolderPathWithinLimit()){
showAlert("Uploading to deep nested folders not allowed");
return;
}

_origOnInputChange.call(this, event);
};

window.DamFileUpload.prototype._confirmUpload = function (event) {
if(!isS7RelativeFolderPathWithinLimit()){
showAlert("Uploading to deep nested folders not allowed");
return;
}

_origConfirmUpload.call(this, event);
};

function isS7RelativeFolderPathWithinLimit(){
var folderPath = window.location.pathname;

if(folderPath.includes("/content/dam")){
folderPath = folderPath.substring(folderPath.indexOf(CONTENT_DAM_PATH) + CONTENT_DAM_PATH.length);
}

return (folderPath.length <= S7_FOLDER_LENGTH_MAX);
}

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

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

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


AEM Cloud Service - Assets server side check for Duplicate file names

$
0
0

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

The client side duplicate check post provides a helpful error message when user tries to upload duplicate files using a browser, however it does not protect when user uploads files using other means, say AEM Desktop app. This post provides a server side way of handling duplicate file names

Demo | Package Install | Github


Error uploading duplicate in Browser



Error uploading duplicate in Desktop App


Solution

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

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


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-server-duplicate-check\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-cs-server-duplicate-check.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a filter apps.experienceaem.assets.core.filters.DuplicateAssetNameCheck executing for .initiateUpload.json and .createasset.html requests to check for duplicate file names across the repo

package apps.experienceaem.assets.core.filters;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.*;

@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM DAM server side duplicate file name check",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=COMPONENT",
"sling.filter.pattern=((.*.initiateUpload.json)|(.*.createasset.html))",
}
)
public class DuplicateAssetNameCheck implements Filter {
private static Logger log = LoggerFactory.getLogger(DuplicateAssetNameCheck.class);

private static String INITIATE_UPLOAD_JSON = ".initiateUpload.json";
private static String CREATE_ASSET_HTML = ".createasset.html";
private static final String EAEM_SERVICE_USER = "eaem-service-user";

@Reference
private ResourceResolverFactory factory;

@Reference
private QueryBuilder builder;

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

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

String uri = slingRequest.getRequestURI();

if(!uri.endsWith(INITIATE_UPLOAD_JSON) && !uri.endsWith(CREATE_ASSET_HTML)){
chain.doFilter(request, response);
return;
}

/*String userAgent = slingRequest.getHeader("User-Agent");

if(isBrowser(userAgent)){
//duplicate filename check in browsers is already done on client side
log.info("A Browser User agent : " + userAgent);
chain.doFilter(request, response);
return;
}*/

String fileNames[] = request.getParameterValues("fileName");

if(fileNames == null){
RequestParameter params[] = slingRequest.getRequestParameters("file");

if(ArrayUtils.isEmpty(params)){
log.warn("Skipping duplicate check, 'fileName' and 'file' params, both are empty");
chain.doFilter(request, response);
return;
}

for (final RequestParameter param : params) {
if (param.getFileName() == null) {
continue;
}

fileNames = new String[1];
fileNames[0] = param.getFileName();

break;
}
}

if(ArrayUtils.isEmpty(fileNames)){
log.warn("Skipping duplicate check, 'fileName' and 'file' params, both are empty");
chain.doFilter(request, response);
return;
}

List<String> duplicatePaths = getDuplicateFilePaths(factory, builder, fileNames);

log.info("duplicatePaths : " + duplicatePaths + ", for file : " + String.join(",", fileNames));

if(!CollectionUtils.isEmpty(duplicatePaths)){
log.info("Duplicate file names detected while upload : " + duplicatePaths);
slingResponse.sendError(SlingHttpServletResponse.SC_FORBIDDEN, "Duplicates found: " + String.join(",", duplicatePaths));
return;
}

chain.doFilter(request, response);
}catch(Exception e){
log.error("Error checking for duplicates", e);
}
}

public static List<String> getDuplicateFilePaths(ResourceResolverFactory resourceResolverFactory,
QueryBuilder builder, String fileNames[]) throws RepositoryException {
ResourceResolver resourceResolver = getServiceResourceResolver(resourceResolverFactory);
List<String> duplicates = new ArrayList<String>();

Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap(fileNames)), resourceResolver.adaptTo(Session.class));

SearchResult result = query.getResult();

for (Hit hit : result.getHits()) {
duplicates.add(hit.getPath());
}

return duplicates;
}

private static Map<String, String> getQueryPredicateMap(String[] fileNames) {
Map<String, String> map = new HashMap<>();
map.put("path", "/content/dam");
map.put("type", "dam:Asset");
map.put("group.p.or", "true");

for(int index = 0; index < fileNames.length; index++){
map.put("group." + index + "_nodename", fileNames[index]);
}

return map;
}

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

private boolean isBrowser(String userAgent){
return (userAgent.contains("Mozilla") || userAgent.contains("Chrome") || userAgent.contains("Safari"));
}

@Override
public void destroy() {
}
}



AEM Cloud Service - Assets custom metadata field for showing external asset usage

$
0
0

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

This post details steps for adding a Custom Metadata Field in Asset Metadata Editor. If an asset is used in external sites, the steps below explain showing asset usage in the external site pages...

Demo | Package Install | Github


External Usage in Metadata Editor


Curl command to post Usage


curl -u local-user:local-user-pass -F"eaem:external-page"="one/this/is/some/external/page/" https://author-p10961-e90064.adobeaemcloud.com/content/dam/experience-aem/am-i-doing-this-right.png/jcr:content/metadata/asset-usage/0


Stored in CRX


Metadata Schema field


Solution

1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-custom-metadata-asset-usage\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config

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

# below registers a namespace with the prefix 'eaem' and the uri 'http://experience-aem/aem'.
register namespace ( eaem ) http://experience-aem/aem
"
]


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-custom-metadata-asset-usage\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-custom-metadata-asset-usage.core:eaem-service-user=[eaem-service-user]]"/>


3) Add the External Asset Usage metadata render script /apps/eaem-custom-metadata-asset-usage/components/asset-external-references/asset-external-references.jsp

<%@ page import="com.adobe.granite.ui.components.Config" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%@ page import="org.apache.sling.api.resource.ResourceResolver" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.Date" %>
<%@include file="/libs/granite/ui/global.jsp" %>
<%@page session="false"%>

<%
final String META_ASSET_USAGE_PATH = "jcr:content/metadata/asset-usage";
final String EAEM_PUBLISH_PAGE = "eaem:external-page";

Config cfg = new Config(resource);
String fieldLabel = cfg.get("fieldLabel", String.class);
String contentPath = (String)request.getAttribute("granite.ui.form.contentpath");

ResourceResolver resolver = slingRequest.getResourceResolver();
Resource eaemResource = resolver.getResource(contentPath);

if(eaemResource == null){
return;
}

Resource metadataRes = eaemResource.getChild(META_ASSET_USAGE_PATH);
Iterator<Resource> usagesItr = ((metadataRes != null) ? metadataRes.getChildren().iterator() : null);

%>
<div>
<label class="coral-Form-fieldlabel">
<h3><%= outVar(xssAPI, i18n, fieldLabel) %></h3>
</label>
<div style="margin-bottom: 10px">

<%
if( (usagesItr == null) || !usagesItr.hasNext()){
%>
<div>None</div>
<%
}else{
while(usagesItr.hasNext()){
Resource usageRes = usagesItr.next();
ValueMap usageResVM = usageRes.getValueMap();
%>
<div>
<%= usageResVM.get(EAEM_PUBLISH_PAGE) %>
</div>
<%
}
}
%>
</div>
</div>


4) Create a schema listener apps.experienceaem.assets.core.listeners.MetadataSchemaListener to update the External Asset Usage field resource type when schema is modified for other purposes. Since there is no custom field create support in schema form builder, we use an existing field eg. Asset Referenced by (dam/gui/coral/components/admin/references) and change it to the custom resource type /apps/eaem-custom-metadata-asset-usage/components/asset-external-references on save using a Osgi Event Listener

package apps.experienceaem.assets.core.listeners;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.lang.StringUtils;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.HashMap;
import java.util.Map;

@Component(service = EventHandler.class,
immediate = true,
property = {
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/ADDED",
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/CHANGED",
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/REMOVED",
EventConstants.EVENT_FILTER + "=" + "(path=/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem/*)"
}
)
public class MetadataSchemaListener implements EventHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());

private static final String SDL_REFERENCES_RES_TYPE = "/apps/eaem-custom-metadata-asset-usage/components/asset-external-references";
private static final String ASSET_USAGE_TITLE = "External Asset Usage";
private static final String EAEM_SERVICE_USER = "eaem-service-user";
private static final String REFERENCES_RES_TYPE = "dam/gui/coral/components/admin/references";

@Reference
private ResourceResolverFactory factory;
@Reference
private QueryBuilder builder;

/**
* Event handler
*
* @param event
*/
public void handleEvent(final Event event) {
logger.debug("Resource event: {} at: {}", event.getTopic(), event.getProperty(SlingConstants.PROPERTY_PATH));
ResourceResolver resourceResolver = getServiceResourceResolver(factory);

try {
Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap()), resourceResolver.adaptTo(Session.class));
SearchResult result = query.getResult();
ValueMap resVM = null;

for (Hit hit : result.getHits()) {
Resource res = resourceResolver.getResource(hit.getPath());

if(res == null){
continue;
}

resVM = res.getValueMap();

if(REFERENCES_RES_TYPE.equals(resVM.get("resourceType", String.class))){
updateReferencesResourceType(res);
}
}
if (resourceResolver.hasChanges()) {
resourceResolver.commit();
}
} catch (RepositoryException | PersistenceException e) {
logger.error("Exception occured at handleEvent() , reason {}", e.getMessage(), e);
}
}

private void updateReferencesResourceType(Resource referencesRes) {
ModifiableValueMap mvm = referencesRes.adaptTo(ModifiableValueMap.class);

if(ASSET_USAGE_TITLE.equals(mvm.get("fieldLabel", String.class))){
mvm.put("resourceType", SDL_REFERENCES_RES_TYPE);
}
}


/**
* Return the query predicate map
*
* @return
*/
private Map<String, String> getQueryPredicateMap() {
Map<String, String> map = new HashMap<>();
map.put("path", "/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem");
map.put("property", "resourceType");
map.put("property.1_value", "dam/gui/coral/components/admin/references");
return map;
}

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


5) If the listener is not executing it might have been blacklisted, may be because its taking too long to finish, error log logs following warning. In such cases increasing the time out in org.apache.felix.eventadmin.Timeout might help....

*WARN* [FelixLogListener] org.apache.felix.eventadmin EventAdmin: Denying event handler from ServiceReference [[org.osgi.service.event.EventHandler] | Bundle(eaem-custom-metadata-asset-usage.core [551])] due to timeout!

AEM Cloud Service - Assets Metadata Editor set Tag Fields Required

$
0
0

Goal

AEM Cloud Version : 2021.2.4944.20210221T230729Z-210225 (Feb 21, 2021)

Add support for Required in Assets Metadata Editor for fields using the Tags widget. Product out of the box does not support required attribute for Tag fields in the schema builder

Demo | Package Install | Github


Tags Required Error


Schema Extension for Required


Solution

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

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


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-meta-tags-required\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-meta-tags-required.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a client library /apps/eaem-meta-tags-required/clientlibs/metadata-tags-required/tags-required-config with categories=dam.admin.ui.coral.schemaeditor.formbuilder.v2 to extend the Metadata Schema Form Builder and add configuration for Tags Required

(function($, $document) {
var TAG_FIELD_RES_TYPE = "cq/gui/components/coral/common/form/tagfield",
RULES_PANEL = "#field-rules",
REQUIRED_CASCADING = "/granite:data/requiredCascading",
F_CONTENT_PATH = "foundation-content-path",
REQUIRED_CHECKBOX_CSS = "eaem-dam-required";

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

function init(){
$document.on("click", ".form-fields > li", function(e) {
e.stopPropagation();
e.preventDefault();

addTagsRequiredConfig(this);
});
}

function addTagsRequiredConfig(field){
var $tagsCheck = $(field).find("[value='" + TAG_FIELD_RES_TYPE + "']"),
$rulesPanel = $(RULES_PANEL);

if(_.isEmpty($tagsCheck) || !_.isEmpty($rulesPanel.find("." + REQUIRED_CHECKBOX_CSS))){
return;
}

var $tagsReadonlyConfig = $(field).find("coral-checkbox[name$='/readOnly']");

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

var configName = $tagsReadonlyConfig.attr("name"),
reqConfigName = configName.substring(0, configName.lastIndexOf("/")) + REQUIRED_CASCADING,
nodeName = configName.substring(0, configName.lastIndexOf("/"));

$tagsReadonlyConfig = $rulesPanel.find("coral-checkbox[name='" + configName + "']");

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

$(getRequiredCheckbox(reqConfigName, isRequiredSet(nodeName))).insertAfter($tagsReadonlyConfig);
}

function isRequiredSet(nodeName){
var schemaPath = $("." + F_CONTENT_PATH).data(F_CONTENT_PATH),
isRequired = false;

if(!schemaPath){
return isRequired;
}

schemaPath = "/bin/querybuilder.json?p.hits=full&p.nodedepth=2&path=" + schemaPath + "&nodename=" + nodeName;

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

isRequired = (data.hits[0]["granite:data"]["requiredCascading"] == "always");
});

return isRequired;
}

function getRequiredCheckbox(configName, checked){
return '<coral-checkbox class="coral-Form-field ' + REQUIRED_CHECKBOX_CSS + '" name="'
+ configName + '"' + (checked ? 'checked' : '') + ' value="always">Required</coral-checkbox>'
+ '<input type="hidden" name="' + configName + '@Delete" value="true">';
}
}(jQuery, jQuery(document)));


4) Form Builder converts the Required checkbox requiredCascading value to true from the set value always during save. To set the value back to always add a metadata schema listener apps.experienceaem.assets.core.listeners.MetadataSchemaListener

package apps.experienceaem.assets.core.listeners;

import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.util.HashMap;
import java.util.Map;

@Component(service = EventHandler.class,
immediate = true,
property = {
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/ADDED",
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/CHANGED",
EventConstants.EVENT_TOPIC + "=" + "org/apache/sling/api/resource/Resource/REMOVED",
EventConstants.EVENT_FILTER + "=" + "(path=/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem/*)"
}
)
public class MetadataSchemaListener implements EventHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());

private static final String EAEM_SERVICE_USER = "eaem-service-user";
private static final String TAG_FIELD_RES_TYPE = "cq/gui/components/coral/common/form/tagfield";
private static final String REQUIRED_CASCADING = "requiredCascading";

@Reference
private ResourceResolverFactory factory;
@Reference
private QueryBuilder builder;

/**
* Event handler
*
* @param event
*/
public void handleEvent(final Event event) {
logger.debug("Resource event: {} at: {}", event.getTopic(), event.getProperty(SlingConstants.PROPERTY_PATH));
ResourceResolver resourceResolver = getServiceResourceResolver(factory);
try {
Query query = builder.createQuery(PredicateGroup.create(getQueryPredicateMap()), resourceResolver.adaptTo(Session.class));
SearchResult result = query.getResult();
ValueMap resVM = null;

for (Hit hit : result.getHits()) {
Resource res = resourceResolver.getResource(hit.getPath());

if(res == null){
continue;
}

resVM = res.getValueMap();

if (TAG_FIELD_RES_TYPE.equals(resVM.get("resourceType", String.class))){
setRequiredCascading(res);
}
}
if (resourceResolver.hasChanges()) {
resourceResolver.commit();
}
} catch (RepositoryException | PersistenceException e) {
logger.error("Exception occured at handleEvent() , reason {}", e.getMessage(), e);
}
}

private void setRequiredCascading(Resource referencesRes) {
ModifiableValueMap mvm = referencesRes.getChild("granite:data").adaptTo(ModifiableValueMap.class);

if("true".equals(mvm.get(REQUIRED_CASCADING, String.class))){
mvm.put(REQUIRED_CASCADING, "always");
}
}

private Map<String, String> getQueryPredicateMap() {
Map<String, String> map = new HashMap<>();
map.put("path", "/conf/global/settings/dam/adminui-extension/metadataschema/experience-aem");
map.put("property", "resourceType");
map.put("property.1_value", "cq/gui/components/coral/common/form/tagfield");
return map;
}

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


5) Metadata editor has support for Required attribute, to trigger it on page load, create a client library /apps/eaem-meta-tags-required/clientlibs/metadata-tags-required/tags-required-in-editor with categories=dam.gui.coral.metadataeditor and add the following code

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

function validateOnLoad(){
var $tagFields = $("foundation-autocomplete[required='true']");

_.each($tagFields, function(tagField){
var validation = $(tagField).adaptTo("foundation-validation");

validation.checkValidity();

validation.updateUI();
})
}
}(jQuery, jQuery(document)));

AEM Cloud Service - Assets Open PDF in new browser tab

$
0
0

Goal

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

Add a button Open PDF in Tab to the Assets Console tool bar for inline opening the selected PDF in a new browser tab

Demo | Package Install | Github


Open PDF Button


PDF In New Tab


Solution

1) Add a proxy servlet apps.experienceaem.assets.core.servlets.PDFProxyServlet to set the content disposition of PDF stream to inline, so user can view the PDF pages in browser...

package apps.experienceaem.assets.core.servlets;

import com.day.cq.dam.api.Asset;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

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

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

Resource pdfRes = request.getResourceResolver().getResource(pdfPath);

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

private void streamPDF(final SlingHttpServletResponse response, final Resource pdfRes) throws Exception {
String fileName = pdfRes.getPath().substring(pdfRes.getPath().lastIndexOf("/") + 1);

response.setContentType("application/pdf");
response.setHeader("Content-disposition", "inline; filename=" + fileName);

Asset asset = pdfRes.adaptTo(Asset.class);
final InputStream in = asset.getOriginal().getStream();

final OutputStream out = response.getOutputStream();

IOUtils.copy(in, out);

out.close();

in.close();
}
}


2) Create the button configuration in /apps/eaem-assets-open-pdf/clientlibs/open-pdf-new-tab/open-pdf-but

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/collection/action"
activeSelectionCount="single"
icon="filePDF"
target=".cq-damadmin-admin-childpages"
text="Open PDF in Tab"
title="Open PDF in new tab"
variant="actionBar"/>


3) Create a clientlib /apps/eaem-assets-open-pdf/clientlibs/open-pdf-new-tab/clientlib with categories=dam.gui.actions.coral for adding the button in Assets Console action bar and open the PDF in new tab when clicked

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

var _ = window._,
initialized = false,
ASSETS_PAGE = "/assets.html", $openPDFBut,
BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-create-activator",
PROXY_PDF_URL = "/bin/eaem/proxy?path=",
OPEN_PDF_BUT_URL = "/apps/eaem-assets-open-pdf/clientlibs/open-pdf-new-tab/open-pdf-but.html";

if (!isAssetsPage()) {
return;
}

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

$document.on("foundation-selections-change", ".foundation-collection", enableOpenPDFButton);

function enableOpenPDFButton(){
if(!$openPDFBut){
return;
}

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

if ($selections.length !== 1) {
$openPDFBut.addClass("foundation-collection-action-hidden");
return;
}

var mimeType = $selections.find(".foundation-collection-assets-meta").data("asset-mimetype");

if(mimeType !== "application/pdf"){
return;
}

$openPDFBut.removeClass("foundation-collection-action-hidden");
}

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

initialized = true;

$.ajax(OPEN_PDF_BUT_URL).done(addOpenPDFButton);
}

function addOpenPDFButton(html) {
html = html || "";

if(!html.trim()){
return;
}

var $eActivator = $("." + BESIDE_ACTIVATOR);

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

$openPDFBut = $("<coral-actionbar-item>" + html + "</coral-actionbar-item>")
.insertAfter($eActivator.closest("coral-actionbar-item"));

$openPDFBut = $openPDFBut.find("button");

$openPDFBut.click(openPDFInNewTab);
}

function openPDFInNewTab() {
var $selections = $(".foundation-selections-item"),
assetPath = $selections.data("foundationCollectionItemId");

window.open(PROXY_PDF_URL + assetPath, '_blank');
}

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


AEM 6580 - Assets Download Version from Timeline

$
0
0

Goal

AEM Assets provides version preview in the timeline, this post extends it to download specific asset version..

Not tested for performance, please test and improve for big asset version downloads....

Demo | Package Install | Github



Solution

1) Create a servlet apps.experienceaem.assets.AssetVersionDownloadServlet to download the specific version...

package apps.experienceaem.assets;

import opennlp.tools.util.StringUtil;
import org.apache.commons.io.IOUtils;
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.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.mime.MimeTypeService;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.servlet.Servlet;
import java.io.InputStream;
import java.io.OutputStream;

@Component(
service = Servlet.class,
property = {
Constants.SERVICE_DESCRIPTION + "= Experience AEM Download Asset Version Servlet",
"sling.servlet.methods=" + HttpConstants.METHOD_GET,
"sling.servlet.paths=" + "/bin/eaem/downloadVersion"
}
)
public class AssetVersionDownloadServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(AssetVersionDownloadServlet.class);

@Reference
transient MimeTypeService mimeTypeService;

protected void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) {
ResourceResolver resolver = request.getResourceResolver();
String resPath = request.getParameter("resource");

Resource assetPathResource = resolver.getResource(resPath + "/jcr:frozenNode/jcr:content/renditions/original/jcr:content");

if(assetPathResource == null){
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}

Node fileNode = assetPathResource.adaptTo(Node.class);
Resource jcrContent = resolver.getResource(resPath + "/jcr:frozenNode/jcr:content");
String mimeType = getMIMEType(jcrContent);

InputStream is = null;

try{
is = fileNode.getProperty("jcr:data").getBinary().getStream();

String fileName = jcrContent.getValueMap().get("cq:name", "");

if(StringUtil.isEmpty(fileName)){
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}

response.setContentType(mimeType);
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");

final OutputStream out = response.getOutputStream();

IOUtils.copy(is, out);

out.close();

is.close();
}catch(Exception e){
log.error("Error retrieving the version for " + resPath, e);
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}

private String getMIMEType(Resource resource){
String cqName = resource.getValueMap().get("cq:name", "");
return mimeTypeService.getMimeType(cqName.substring(cqName.indexOf('.') + 1));
}
}


2) Create a clientlib /apps/eaem-assets-download-version/clientlib with categories=cq.gui.coral.common.admin.timeline to add the Download Version button and necessary download logic...

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

$document.on("click", TIME_LINE_EVENT_CSS, addDownloadVersion);

function addDownloadVersion(){
var $timeLineButton = $(this).find(REVERT_TO_VERSION_SEL),
$timelineForm = $timeLineButton.closest("form");

if(!_.isEmpty($timelineForm.find("." + EAEM_VERSION_CSS))){
return;
}

$(getDownloadButtonHtml()).appendTo($timelineForm).click(downloadVersion);
}

function downloadVersion(){
var $dataPreview = $(this).closest("[data-preview]"),
versionPath = $dataPreview.data("preview");

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

window.open("/bin/eaem/downloadVersion?resource=" + versionPath, '_blank');
}

function getDownloadButtonHtml(){
return '<button is="coral-button" class="coral3-Button coral3-Button--secondary ' + EAEM_VERSION_CSS +
'" type="button" size="M" variant="secondary" style="width:100%; margin-top: 0.2rem;">' +
'<coral-button-label>Download Version</coral-button-label>' +
'</button>'
}
})(jQuery, jQuery(document));


AEM Cloud Service - Sites Component Insert and Delete via Content Tree

$
0
0

Goal

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

Add buttons to Delete and Insert components via Sites Content tree

Demo | Package Install | Github


Solution

Create a clientlib /apps/eaem-sites-content-tree-delete-insert/clientlibs/content-tree with categories=cq.authoring.dialog.all and below code to add the delete and insert icons in content tree and execute necessary api

(function ($, ns, channel) {
var _orignTitleFn, _orignBindFn;

channel.on("cq-editor-loaded", $.debounce(500, false, extendContentTree));

function extendContentTree(){
var contentTree = Granite.author.ui.contentTree;

_orignTitleFn = contentTree._getElementTitle;
_orignBindFn = contentTree._bindListeners;

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

contentTree._getElementTitle = getElementTitle;

contentTree._bindListeners = bindListeners;
}

function getElementTitle(editable, componentTitle){
var titleHtml = _orignTitleFn.call(this, editable, componentTitle),
buttonDeleteHtml = '<coral-icon icon="delete" size="M" class="eaem-contenttree-delete" data-content-align="Top"></coral-icon>',
buttonInsertHtml = '<coral-icon icon="add" size="M" class="eaem-contenttree-insert" data-content-align="Top"></coral-icon>',
padding = '<span style="margin-left: 20px"></span>';

if (titleHtml) {
if (editable.name == "responsivegrid") {
//dont add delete or insert
}else if (!editable.config.isContainer) {
titleHtml = titleHtml + padding + buttonDeleteHtml;
}else{
titleHtml = titleHtml + padding + buttonDeleteHtml + buttonInsertHtml;
}
}

return titleHtml;
}

function bindListeners(){
var editables = this.editables;

_orignBindFn.call(this);

$(".eaem-contenttree-insert").click(function(event){
var treeItem = event.currentTarget.closest("coral-tree-item"),
editable = editables.find(treeItem.value)[0];

ns.edit.ToolbarActions.INSERT.execute(editable);
});

$(".eaem-contenttree-delete").click(function (event) {
var treeItem = event.currentTarget.closest("coral-tree-item"),
editable = editables.find(treeItem.value)[0];

if (editable && editable.overlay && editable.overlay.dom && !editable.overlay.isDisabled()
&&!editable.overlay.isSelected()) {
editable.overlay.dom.focus().trigger("click");
}

if (editable) {
ns.edit.ToolbarActions.DELETE.execute(editable);
}
});
}
}(jQuery, Granite.author, jQuery(document)));


Viewing all 525 articles
Browse latest View live


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