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

AEM 6420 - Assets PDF Insights - Importing PDF Analytics into AEM

$
0
0

Goal


Before importing analytics data into AEM you need to set up AEM for Adobe Analytics. For configuring Adobe Launch and Adobe Analytics in AEM check this post

For Content Fragments Insights check this post


AEM was now successfully integrated with Analytics, the next steps are...

1. Create a data layer in AEM page component head for storing pdf path (link on page for download)

2. Create a new HTL component to render the PDF download link on page

3. Refresh page, click on download link and make sure the pdf path is sent to Analytics in evar variable evar1 (v1)

4. Setup a polling importer to import Analytics data into AEM at regular intervals

5. Add the data in jcr:content/performance@dam:impressionCount (otb Impressions column in Assets List View displays this value)

Demo | Package Install | Github


List View in AEM








PDF Views Report in Analytics





Configure Adobe Launch

1) First step is add Rules and Data Elements in Adobe Launch to get data from the custom data layer (created in next section). In the launch configuration exercise, we added necessary configuration for content fragments, here we add the configuration for capturing downloaded pdf path

2) In the Adobe Context Hub Launch extension, add pdfPath json schema element and create a data element pdf_on_page


3) In Rules section, add a new rule to set the Analytics variable eVar1 when user clicks on link with css selector .eaem-pdf-link-click which is the pdf download link (in the next section we'll create an AEM component to set this css class on download link). The assumption here, there is only one download PDF link on page



4) Here we don't configure a Send Beacon action as clicking on download link sends a beacon; we just set  the eVar1 (v1) value to pdf path that is sent along... Publish the changes to Production Launch (only for demonstration, always test on Stage Launch and Publish to Production Launch...)



Add Data Layer in Page Template

1) Time to setup the data layer window.eaemData in your page head.html with the page path and pdf path data structure (the data layer root window.eaemData was setup in Adobe Launch extension Context Hub in the this exercise) pdfPath schema added in previous section for this post

For simplicity, attached package install contains the core components page /apps/core/wcm/components/page/v2/page/head.html modified to add the data layer, ideally you'd be extending the core components page component or add it in a new page component

<script>
window.eaemData = {
page : {
pageInfo : {
pagePath : "${currentPage.path @ context='scriptString'}"
},
contentFraments:{
paths : [ ]
},
pdfPath: undefined
}
}
</script>


2) The download pdf component rendered on page can now set PDF path with window.eaemData.page.pdfPath = "some_pdf_path_in_aem_assets"; 


Download PDF Component

1) Create a new component /apps/eaem-assets-pdf-insights/components/eaem-download-pdf-component to create the download link for selected PDF



2) Add the following code in /apps/eaem-assets-pdf-insights/components/eaem-download-pdf-component/eaem-download-pdf-component.html. Here the download link anchor is set with css class eaem-pdf-link-click so Adobe Launch can trigger Analytics Set Variables action when user clicks on the link

<div style="margin: 10px 0 10px 0">
<div data-sly-test="${!properties.eaemPDFPath}" data-sly-unwrap>
No PDF selected to View / Download
</div>
<div data-sly-test="${properties.eaemPDFPath}" data-sly-unwrap>
Click on the link to
<a class="eaem-pdf-link-click" href="${properties.eaemPDFPath}" download>
View / Download "${properties.eaemTitle}"
</a>
<script>
window.eaemData.page.pdfPath = "${properties.eaemPDFPath @ context='scriptString'}";
</script>
</div>
</div>


Verify PDF Path sent to Analytics

1) Drag and drop the component created above EAEM PDF Component and select a pdf



2) Refresh the page in published mode (wcmmode=disabled), click on the download PDF link and check the pdf path sent to Analytics



Setup Polling Importer

1) Create Polling Importer apps.experienceaem.assets.PDFViewsImporter extending com.day.cq.polling.importer.Importer with custom scheme eaemReport, add the following code...

package apps.experienceaem.assets;

import com.day.cq.analytics.sitecatalyst.SitecatalystUtil;
import com.day.cq.analytics.sitecatalyst.SitecatalystWebservice;
import com.day.cq.polling.importer.ImportException;
import com.day.cq.polling.importer.Importer;
import com.day.cq.wcm.webservicesupport.Configuration;
import com.day.cq.wcm.webservicesupport.ConfigurationManager;
import com.day.cq.wcm.webservicesupport.ConfigurationManagerFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
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 javax.jcr.Node;
import javax.jcr.Session;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;

@Component(
immediate = true,
service = {Importer.class},
property = {
Importer.SCHEME_PROPERTY + "=" + PDFViewsImporter.SCHEME_VALUE
}
)
@Designate(ocd = PDFViewsImporter.PDFViewsImporterConfiguration.class)
public class PDFViewsImporter implements Importer {
private Logger log = LoggerFactory.getLogger(getClass());

public static final String SCHEME_VALUE = "eaemReport";
private static final String USE_ANALYTICS_FRAMEWORK = "use-analytics-framework";
private static final String GET_ANALYTICS_FOR_LAST_DAYS = "get-analytics-for-last-days";
private static final String UPDATE_ANALYTICS_SERVICE = "eaem-update-pdf-with-analytics";
private static final long DEFAULT_REPORT_FETCH_DELAY = 10000;
private static final long DEFAULT_REPORT_FETCH_ATTEMPTS = 10;
private long reportFetchDelay = DEFAULT_REPORT_FETCH_DELAY;
private long reportFetchAttempts = DEFAULT_REPORT_FETCH_ATTEMPTS;

@Reference
private SitecatalystWebservice sitecatalystWebservice;

@Reference
private ResourceResolverFactory resolverFactory;

@Reference
private ConfigurationManagerFactory cfgManagerFactory;

@Reference
private SlingSettingsService settingsService;

@Activate
protected void activate(PDFViewsImporterConfiguration configuration) {
reportFetchDelay = configuration.reportFetchDelay();
reportFetchAttempts = configuration.reportFetchAttempts();
}

public void importData(String scheme, String dataSource, Resource target) throws ImportException {
log.info("Importing analytics data for evar - " + dataSource);

try {
String useAnalyticsFrameworkPath = target.getValueMap().get(USE_ANALYTICS_FRAMEWORK, String.class);

if(StringUtils.isEmpty(useAnalyticsFrameworkPath)){
log.warn("Analytics framework path property " + USE_ANALYTICS_FRAMEWORK + " missing on " + target.getPath());
return;
}

Configuration configuration = getConfiguration(useAnalyticsFrameworkPath, target.getResourceResolver());

String reportSuiteID = SitecatalystUtil.getReportSuites(settingsService, configuration);

log.debug("Report Suite ID - " + reportSuiteID);

JSONObject reportDescription = getReportDescription(target, dataSource, reportSuiteID);

String queueReportResponse = sitecatalystWebservice.queueReport(configuration, reportDescription);

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

JSONObject jsonObj = new JSONObject(queueReportResponse);

Long queuedReportId = jsonObj.optLong("reportID");

if(queuedReportId == 0L) {
log.error("Could not queue report, queueReportResponse - " + queueReportResponse);
return;
}

boolean reportReady = false;
JSONObject report = null;

for(int attemptNo = 1; attemptNo <= reportFetchAttempts; ++attemptNo) {
log.debug("Attempt number " + attemptNo + " to fetch queued report " + queuedReportId);

String reportData = sitecatalystWebservice.getReport(configuration, queuedReportId.toString());

log.debug("Get report " + queuedReportId + " result: " + reportData);

jsonObj = new JSONObject(reportData);
String errorResponse = jsonObj.optString("error");

reportReady = ((errorResponse == null) || !"report_not_ready".equalsIgnoreCase(errorResponse));

if(reportReady) {
report = jsonObj.optJSONObject("report");
break;
}

Thread.sleep(reportFetchDelay);
}

if(report == null) {
log.error("Could not fetch queued report [" + queuedReportId + "] after " + reportFetchAttempts + " attempts");
}

ResourceResolver rResolverForUpdate = resolverFactory.getServiceResourceResolver(
Collections.singletonMap("sling.service.subservice", (Object)UPDATE_ANALYTICS_SERVICE));

saveAnalyticsData(report, rResolverForUpdate);

log.info("Successfully imported analytics data with report id - " + queuedReportId);
}catch(Exception e){
log.error("Error importing analytics data for list var - " + dataSource, e);
}
}

private void saveAnalyticsData(JSONObject report, ResourceResolver resolver ) throws Exception{
JSONArray data = report.optJSONArray("data");
JSONObject metrics = null;
String pdfPath;

Resource pdfResource;
Node pdfJcrContent;
ModifiableValueMap modifiableValueMap = null;

for(int d = 0, len = data.length(); d < len; d++){
metrics = data.getJSONObject(d);
pdfPath = metrics.getString("name");

pdfResource = resolver.getResource(pdfPath);

if(pdfResource == null){
log.warn("PDF Asset not found - " + pdfPath);
continue;
}

Resource performanceRes = ResourceUtil.getOrCreateResource(pdfResource.getResourceResolver(),
pdfPath + "/" + "jcr:content" + "/" + "performance", (String)null, (String)null, false);

modifiableValueMap = performanceRes.adaptTo(ModifiableValueMap.class);

modifiableValueMap.put("dam:impressionCount", metrics.getJSONArray("counts").getString(0));
modifiableValueMap.put("jcr:lastModified", Calendar.getInstance());
}

resolver.adaptTo(Session.class).save();
}

private JSONObject getReportDescription(Resource target, String dataSource, String reportSuiteID) throws Exception{
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

Integer daysCount = target.getValueMap().get(GET_ANALYTICS_FOR_LAST_DAYS, Integer.class);

if(daysCount == null){
daysCount = -365;
}

JSONObject reportDescription = new JSONObject();

reportDescription.put("elements", getElements(dataSource));
reportDescription.put("metrics", getMetrics());
reportDescription.put("reportSuiteID", reportSuiteID);
reportDescription.put("dateTo", formatter.format(cal.getTime()));

cal.add(Calendar.DAY_OF_MONTH, daysCount);
reportDescription.put("dateFrom", formatter.format(cal.getTime()));

return reportDescription;
}

private JSONArray getElements(String dataSource) throws Exception{
JSONObject elements = new JSONObject();

elements.put("id", dataSource);
elements.put("top", 10000);
elements.put("startingWith", 1);

return new JSONArray().put(elements);
}

private JSONArray getMetrics() throws Exception{
JSONObject metrics = new JSONObject();

metrics.put("id", "pageviews");

return new JSONArray().put(metrics);
}

private Configuration getConfiguration(String analyticsFrameworkPath, ResourceResolver resourceResolver)
throws Exception {
ConfigurationManager cfgManager = cfgManagerFactory.getConfigurationManager(resourceResolver);

String[] services = new String[]{ analyticsFrameworkPath };

return cfgManager.getConfiguration("sitecatalyst", services);
}


public void importData(String scheme,String dataSource,Resource target,String login,String password)
throws ImportException {
importData(scheme, dataSource, target);
}

@ObjectClassDefinition(
name = "EAEM Analytics Report Importer",
description = "Imports Analytics List Variable Reports periodically into AEM"
)
public @interface PDFViewsImporterConfiguration {
@AttributeDefinition(
name = "Fetch delay",
description = "Number in milliseconds between attempts to fetch a queued report. Default is set to 10000 (10s)",
defaultValue = {"" + DEFAULT_REPORT_FETCH_DELAY},
type = AttributeType.LONG
) long reportFetchDelay() default DEFAULT_REPORT_FETCH_DELAY;

@AttributeDefinition(
name = "Fetch attempts",
description = "Number of attempts to fetch a queued report. Default is set to 10",
defaultValue = {"" + DEFAULT_REPORT_FETCH_ATTEMPTS},
type = AttributeType.LONG
) long reportFetchAttempts() default DEFAULT_REPORT_FETCH_ATTEMPTS;
}
}

2) Create a managed poll configuration sling:OsgiConfig /apps/eaem-assets-pdf-insights/config/com.day.cq.polling.importer.impl.ManagedPollConfigImpl-eaem-pdf-insights and set id as eaem-evar-pdf-report-days source as evar1 among other values. Here we trying to pull a report created, with data collected in custom variable evar1 which is configured for PDF Views in Analytics

<?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"
enabled="{Boolean}true"
id="eaem-evar-pdf-report-days"
interval="{Long}86400"
reference="{Boolean}true"
source="eaemReport:evar1"/>

3) Create cq:PollConfigFolder /etc/eaem-pdf-insights/analytics-evars-report-poll-config with a reference to the managed poll configuration created above eaem-evar-pdf-report-days

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:rep="internal"
jcr:mixinTypes="[rep:AccessControllable]"
jcr:primaryType="cq:PollConfigFolder"
managedPollConfig="eaem-evar-pdf-report-days"
use-analytics-framework="/etc/cloudservices/sitecatalyst/ags-959-analytics/experience-aem-analytics-framework"
get-analytics-for-last-days="{Long}-365"
source="eaemReport:evar1"/>

4) In the above poll config folder configuration, we are also adding analytics framework path /etc/cloudservices/sitecatalyst/ags-959-analytics/experience-aem-analytics-framework set with use-analytics-framework created in previous exercise and configuring property get-analytics-for-last-days to get the last 365 days data

5) Create a service user mapping /apps/eaem-assets-pdf-insights/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-eaem-pdf-insights with write access to update PDF nodes, here we create the node performance and set property dam:impressionCount

<?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="[apps.experienceaem.assets.eaem-assets-pdf-insights-bundle:eaem-update-pdf-with-analytics=dam-update-service]"/>

6) The polling importer configuration is set to import every 24 hours, if you'd like to change it for quick testing, change it in OSGI config manager http://localhost:4502/system/console/configMgr




List View Settings

1)  In the List view enable Impressions column



2) The impressions count imported by PDFViewsImporter into jcr:content/performance@dam:impressionCount is displayed using otb list view framework



AEM 6420 - Invalidate Dynamic Media Scene7 CDN Cache in AEM Workflow step

$
0
0

Goal


In an AEM 6420 instance with sling run mode dynamicmedia_scene7 invalidate the Scene7 CDN cache programmatically using AEM workflow process step

For configuring Dynamic Media Scene 7 check this post

Package Install | Github

Thank you unknown coders for the snippets...


Invalidate CDN in Scene7



Request sent by WF step

Response from Scene7


Solution


1) Create a workflow process step apps.experienceaem.assets.EAEMScene7InvalidateCDNCache with the following code...

package apps.experienceaem.assets;

import com.adobe.granite.crypto.CryptoSupport;
import com.adobe.granite.license.ProductInfo;
import com.adobe.granite.license.ProductInfoService;
import com.adobe.granite.workflow.PayloadMap;
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.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.dam.scene7.api.*;
import com.scene7.ipsapi.AuthHeader;
import com.scene7.ipsapi.CdnCacheInvalidationParam;
import com.scene7.ipsapi.UrlArray;
import org.apache.commons.io.IOUtils;
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.http.osgi.services.HttpClientBuilderFactory;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.jcr.Session;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.List;

@Component(
immediate = true,
service = {WorkflowProcess.class},
property = {
"process.label = EAEM Scene7 Invalidate CDN Cache"
}
)
public class EAEMScene7InvalidateCDNCache implements WorkflowProcess {
private static final Logger LOG = LoggerFactory.getLogger(EAEMScene7InvalidateCDNCache.class);

private static final String EAEM_CACHE_INVALIDATION_PROCESS = "eaem-cache-invalidation-process";

@Reference
private ResourceResolverFactory resourceResolverFactory;

@Reference
private Scene7APIClient scene7APIClient;

@Reference
private Scene7Service scene7Service;

@Reference
private S7ConfigResolver s7ConfigResolver;

@Reference
private CryptoSupport cryptoSupport;

@Reference
private ProductInfoService productInfoService;

@Reference
private Scene7EndpointsManager scene7EndpointsManager;

public void execute(final WorkItem workItem, final WorkflowSession workflowSession, final MetaDataMap arg)
throws WorkflowException {
try {
Asset asset = getAssetFromPayload(workItem, workflowSession.adaptTo(Session.class));

ResourceResolver s7ConfigResourceResolver = resourceResolverFactory.getServiceResourceResolver(
Collections.singletonMap("sling.service.subservice", (Object)EAEM_CACHE_INVALIDATION_PROCESS));

S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, asset.getPath());

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

CdnCacheInvalidationParam cacheInvalidationParam = getCdnCacheInvalidationParam(s7Config, asset);

String response = makeCDNInvalidationRequest(s7Config, cacheInvalidationParam);

if(!response.contains("<invalidationHandle>")){
throw new Exception("Error invalidating CDN cache, Response does not contain <invalidationHandle/> element");
}
} catch (Exception e) {
LOG.error("Error while invalidating CDN cache", e);
}
}

public CdnCacheInvalidationParam getCdnCacheInvalidationParam(S7Config s7Config, Asset asset) throws Exception{
String cdnInvTemplates = getCDNInvalidationTemplate(s7Config);

LOG.debug("Scene7 CDN Invalidate template - " + cdnInvTemplates);

String[] invalidatePaths = getCDNInvalidationPathsForAssets(cdnInvTemplates, asset).split("\n");

UrlArray urlArray = new UrlArray();

List<String> items = urlArray.getItems();

Collections.addAll(items, invalidatePaths);

CdnCacheInvalidationParam cdnCacheInvalidationParam = new CdnCacheInvalidationParam();

cdnCacheInvalidationParam.setCompanyHandle(s7Config.getCompanyHandle());

cdnCacheInvalidationParam.setUrlArray(urlArray);

LOG.debug("Scene7 CDN Invalidate Paths - " + invalidatePaths);

return cdnCacheInvalidationParam;
}

public String makeCDNInvalidationRequest(S7Config s7Config, CdnCacheInvalidationParam cacheInvalidationParam)
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);

Marshaller marshaller = getMarshaller(AuthHeader.class);
StringWriter sw = new StringWriter();
marshaller.marshal(authHeader, sw);
String authHeaderStr = sw.toString();

marshaller = getMarshaller(cacheInvalidationParam.getClass());
sw = new StringWriter();
marshaller.marshal(cacheInvalidationParam, 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 = "";

try {
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.debug("Scene7 CDN Invalidation response - " + responseBody);
}finally{
if(client != null){
client.close();
}
}

return responseBody;
}

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

public String getCDNInvalidationPathsForAssets(String template, Asset asset) throws Exception{
String scene7ID = asset.getMetadataValue("dam:scene7ID");
return template.replaceAll("<ID>", scene7ID);
}

public String getCDNInvalidationTemplate(S7Config s7Config) throws Exception{
String appSettingsTypeHandle = scene7Service.getApplicationPropertyHandle(s7Config);

if(appSettingsTypeHandle == null) {
return "";
} else {
Document document = scene7APIClient.getPropertySets(appSettingsTypeHandle, s7Config);

return getPropertyValue(document, "application_cdn_invalidation_template");
}
}

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

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

XPathExpression xpathExpr = xpath.compile(expression);

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

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

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

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

value = valueNode.getTextContent();

break;
}

return value;
}

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

public Asset getAssetFromPayload(final WorkItem item, final Session session) throws Exception{
Asset asset = null;

if (!item.getWorkflowData().getPayloadType().equals(PayloadMap.TYPE_JCR_PATH)) {
return null;
}

final String path = item.getWorkflowData().getPayload().toString();
final Resource resource = getResourceResolver(session).getResource(path);

if (null != resource) {
asset = DamUtil.resolveToAsset(resource);
} else {
LOG.error("getAssetFromPayload: asset [{}] in payload of workflow [{}] does not exist.", path,
item.getWorkflow().getId());
}

return asset;
}

private ResourceResolver getResourceResolver(final Session session) throws LoginException {
return resourceResolverFactory.getResourceResolver( Collections.<String, Object>
singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session));
}
}


2) Create a service user mapping for reading the DMS7 configuration /apps/eaem-scene7-invalidate-cdn-cache/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-eaem-cdn-invalidate

<?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="[apps.experienceaem.assets.eaem-scene7-invalidate-cdn-cache-bundle:eaem-cache-invalidation-process=scene7-config-service]"/>

3) Create a workflow model /conf/global/settings/workflow/models/eaem-dms7-invalidate-cdn-cache with step EAEM DMS7 Invalidate CDN Cache and sync the model to /var/workflow/models/eaem-dms7-invalidate-cdn-cache



4) To invalidate the CDN cache of any Dynamic Media asset, select the asset and start workflow EAEM DMS7 Invalidate CDN Cache




AEM 6440 - Touch UI Composite Image Multifield

$
0
0

Goal


Before trying out this extension, check if the AEM Core components Carousel serves your purpose - http://opensource.adobe.com/aem-core-wcm-components/library/carousel.html or using a Pathbrowser is good enough - /libs/granite/ui/components/coral/foundation/form/pathbrowser

Create  a Touch UI Composite Multifield configuration supporting Images, widgets of type /libs/cq/gui/components/authoring/dialog/fileupload

For AEM 62 check this post

Demo | Package Install | Github


Component Rendering




Image Multifield Structure




Nodes in CRX




Dialog



Dialog XML

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="64 Image Multifield"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<products
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Products">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./products">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<product
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Name of Product"
fieldLabel="Product Name"
name="./product"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
<file
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/fileupload"
allowUpload="false"
autoStart="{Boolean}false"
class="cq-droptarget"
fileNameParameter="./fileName"
fileReferenceParameter="./fileReference"
mimeTypes="[image/gif,image/jpeg,image/png,image/webp,image/tiff]"
multiple="{Boolean}false"
name="./file"
title="Upload Image Asset"
uploadUrl="${suffix.path}"
useHTML5="{Boolean}true"/>
</items>
</column>
</items>
</field>
</products>
</items>
</column>
</items>
</content>
</jcr:root>


Solution


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

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

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

                         image-multifield.js

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

(function ($, $document) {
var COMPOSITE_MULTIFIELD_SELECTOR = "coral-multifield[data-granite-coral-multifield-composite]",
FILE_REFERENCE_PARAM = "fileReference",
registry = $(window).adaptTo("foundation-registry"),
ALLOWED_MIME_TYPE = "image/jpeg",
adapters = registry.get("foundation.adapters");

var fuAdapter = _.reject(adapters, function(adapter){
return ((adapter.type !== "foundation-field") || (adapter.selector !== "coral-fileupload.cq-FileUpload"));
});

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

fuAdapter = fuAdapter[0];

var orignFn = fuAdapter.adapter;

fuAdapter.adapter = function(el) {
return Object.assign(orignFn.call(el), {
getName: function () {
return el.name;
},
setName: function(name) {
var prefix = name.substr(0, name.lastIndexOf(el.name));

el.name = name;

$("input[type='hidden'][data-cq-fileupload-parameter]", el).each(function(i, el) {
if ($(el).data("data-cq-fileupload-parameter") !== "filemovefrom") {
this.setAttribute("name", prefix + this.getAttribute("name"));
}
});
}
});
};

$document.on("foundation-contentloaded", function(e) {
var composites = $(COMPOSITE_MULTIFIELD_SELECTOR, e.target);

composites.each(function() {
Coral.commons.ready(this, function(el) {
addThumbnails(el);
});
});
});

function addThumbnails(multifield){
var $multifield = $(multifield),
dataPath = $multifield.closest(".cq-dialog").attr("action"),
mfName = $multifield.attr("data-granite-coral-multifield-name");

dataPath = dataPath + "/" + getStringAfterLastSlash(mfName);

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

function handler(mfData){
multifield.items.getAll().forEach(function(item, i) {
var $mfItem = $(item),
$fileUpload = $mfItem.find("coral-fileupload");

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

var itemName = getJustItemName($fileUpload.attr("name"));

if(_.isEmpty(mfData[itemName]) || _.isEmpty((mfData[itemName][FILE_REFERENCE_PARAM]))){
return;
}

var imagePath = mfData[itemName][FILE_REFERENCE_PARAM];

$fileUpload.trigger($.Event("assetselected", {
path: imagePath,
group: "",
mimetype: ALLOWED_MIME_TYPE, // workaround to add thumbnail
param: "",
thumbnail: getThumbnailHtml(imagePath)
}));
});
}

function getThumbnailHtml(path){
return "<img class='cq-dd-image' src='" + path + "/_jcr_content/renditions/cq5dam.thumbnail.319.319.png'>";
}

function getJustItemName(itemName){
itemName = itemName.substr(itemName.indexOf(mfName) + mfName.length + 1);

itemName = itemName.substring(0, itemName.indexOf("/"));

return itemName;
}
}

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

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


5) To render the composite multifield items eg. to create a Image Gallery component, create a HTL render script /apps/eaem-touchui-image-multifield/sample-image-multifield/sample-image-multifield.html

<div>
<b>6420 Composite Image Multifield</b>

<div data-sly-use.company="helper.js" data-sly-unwrap>
<div data-sly-test="${!company.products && wcmmode.edit}">
Add products using component dialog
</div>

<div data-sly-test="${company.products}">
<div data-sly-list.product="${company.products}">
<div>
<div>${product.name}</div>
<div>${product.path}</div>
<div><img src="${product.fileReference}" width="150" height="150"/></div>
</div>
</div>
</div>
</div>
</div>


6) Finally a HTL use-script to read the multifield data /apps/eaem-touchui-image-multifield/sample-image-multifield/helper.js

"use strict";

use( ["/libs/wcm/foundation/components/utils/ResourceUtils.js","/libs/sightly/js/3rd-party/q.js" ], function(ResourceUtils, Q){
var prodPromise = Q.defer(), company = {},
productsPath = granite.resource.path + "/products";

company.products = undefined;

ResourceUtils.getResource(productsPath)
.then(function (prodParent) {
return prodParent.getChildren();
})
.then(function(products) {
addProduct(products, 0);
});

function addProduct(products, currIndex){
if(!company.products){
company.products = [];
}

if (currIndex >= products.length) {
prodPromise.resolve(company);
return;
}

var productRes = products[currIndex],
properties = productRes.properties;

var product = {
path: properties.path,
name: properties.product,
fileReference: properties.fileReference
};

company.products.push(product);

addProduct(products, (currIndex + 1));
}

return prodPromise.promise;
} );



AEM 6440 - Content Fragment Editor Set Multi line text Required

$
0
0

Goal


Set the Multi line text widget required (Plain / Markdown / Richtext)

Demo | Package Install | Github


Multi Line in Model



Set Required in CRX Manually



Content Fragment Editor Error



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-cfm-multi-line-text-required-validator

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-cfm-multi-line-text-required-validator and set a property categories of String type to dam.cfm.authoring.contenteditor.v2dependencies of type String[] with value underscore

3) Create file ( type nt:file ) /apps/eaem-cfm-multi-line-text-required-validator/clientlib/js.txt, add the following

                         multiline-required.js

4) Create file ( type nt:file ) /apps/eaem-cfm-multi-line-text-required-validator/clientlib/multiline-required.js, add the following code

(function ($, $document) {
var CFM = window.Dam.CFM,
EAEM_INVISIBLE_CLASS = "eaem-text-invisible",
PLAIN_MARKDOWN_SELECTOR = "textarea.plaintext-editor, textarea.markdown-editor",
registry = $(window).adaptTo("foundation-registry");

//for plain text, markdown
registry.register("foundation.validation.validator", {
selector: PLAIN_MARKDOWN_SELECTOR,
validate: validator
});

//for richtext
registry.register("foundation.validation.validator", {
selector: "." + EAEM_INVISIBLE_CLASS,
validate: validator,
show: function (invisibleText, message) {
$(invisibleText).prevAll("[data-cfm-richtext-editable]").css("border-color", "#e14132");
},
clear: function (invisibleText) {
$(invisibleText).prevAll("[data-cfm-richtext-editable]").css("border-color", "#d0d0d0");
}
});

CFM.Core.registerReadyHandler(registerRTEValidator);

function registerRTEValidator(){
var $cfmMultiEditor = $(".cfm-multieditor");

if ($cfmMultiEditor.find(".coral-Form-field").attr("aria-required") !== "true") {
return;
}

var $multiLineLabel = $cfmMultiEditor.find(".cfm-multieditor-embedded-label");

$multiLineLabel.html($multiLineLabel.html() + " *");

var $rte = $("textarea.rte-sourceEditor");

if(!_.isEmpty($rte)){
//coral validation framework ignores hidden and contenteditable fields, so add an invisible text field
//the text field is just for registering a validator
var $eaemCopyField = $("<input type=text style='display:none' class='" + EAEM_INVISIBLE_CLASS + "'/>")
.insertAfter($rte);

var $rteEditable = $rte.prev("[data-cfm-richtext-editable]");

$eaemCopyField.val($rteEditable.text().trim());

checkValidity.call($eaemCopyField[0]);

$rte.prev("[data-cfm-richtext-editable]").on("input", function() {
$eaemCopyField.val($(this).text().trim());
checkValidity.call($eaemCopyField[0]);
});
}else{
//$(PLAIN_MARKDOWN_SELECTOR).each(checkValidity);
}
}

function checkValidity() {
var foundationValidation = $(this).adaptTo("foundation-validation");
foundationValidation.checkValidity();
foundationValidation.updateUI();
}

function validator(element){
var $element = $(element),
$cfmMultiLineField = $element.closest(".cfm-multieditor").find(".coral-Form-field");

if ($cfmMultiLineField.attr("aria-required") !== "true") {
return;
}

if (!_.isEmpty($element.val())) {
return;
}

return Granite.I18n.get("Please fill out this field.");
}
}(jQuery, jQuery(document)));


AEM 6420 - Sync Specific Folders to Scene7 in AEM Dynamic Media Scene7 (DMS7)

$
0
0

Goal


By default, the Scene7 step of DAM Update Asset workflow syncs all assets uploaded to AEM, to Scene 7, in DMS7 mode (sling run mode dynamicmedia_scene7)

If the requirement is syncing specific folders, this post explains how-to, by adjusting the DAM Update Asset Workflow

Demo | Package Install | Github


Solution


1) Edit the DAM Update Asset workflow to add an OR Split step, move the Scene7 step to branch 1 and add a No Operation step in branch 2



2) Add the following script in branch 1 tab of OR Split; add the folder paths in SYNC_PATHS variable, which should be kept in sync with Scene7 (so the images and videos uploaded to these folders ONLY are synced to Scene7)

function check() {
var SYNC_PATHS = new Packages.java.util.ArrayList();

//add the paths you'd like to sync to scene7
SYNC_PATHS.add("/content/dam/sreek");

var doSync = false;

try {
var path = workflowData.getPayload().toString();

if (path === null) {
return doSync;
}

path = path.replaceAll("/jcr:content/renditions/original", "");

log.info("Checking if asset should be uploaded to Scene7 - " + path);

var payloadNode = workflowSession.getSession().getNode(path);

if (payloadNode === null) {
return doSync;
}

doSync = SYNC_PATHS.contains(payloadNode.getParent().getPath());

if (doSync === false) {
log.info("Skipping upload to Scene7 - " + path);
} else {
log.info("Uploading to Scene7 - " + path);
}
} catch (e) {
doSync = false;
log.error(e);
}

return doSync;
}




3) For the branch 2 tab (no operation) add the following script

function check(){
return true;
}


4) Sync the workflow to copy the model from /conf/global/settings/workflow/models/dam/update_asset to /var/workflow/models/dam/update_asset

5) A bug in 64 SP2 causes the workflow nodes to get reversed as below during workflow sync (fixed in later versions), so workaround it in /var/workflow/models/dam/update_asset by changing the node xml and pushing to CRX

                           Before:


                           After:



6) Upload an asset to folder with Scene7 sync allowed, you can see the jcr:content/metadata@dam:scene7ID  in asset metadata, confirming the upload to Scene7



7) Assets in folders that are skipped by OR-split do not have  jcr:content/metadata@dam:scene7ID  in asset metadata and are not uploaded to Scene7




AEM 65 - Sync Specific Folders to Scene7 in AEM Dynamic Media Scene7 (DMS7), Show Indicator in Card View

$
0
0

Goal


By default, the Scene7 step of DAM Update Asset workflow syncs all assets uploaded to AEM, to Scene 7, in DMS7 mode (sling run mode dynamicmedia_scene7)

If the requirement is syncing specific folders, this post explains how-to, by adjusting the DAM Update Asset Workflow and show a UI indicator in card view (an improvement to the solution posted for 6420)

Another way of do this is by removing Scene7 step from default DAM Update Asset workflow, creating a DAM update workflow with Scene7 step having workflow launcher path condition set to e.g. /content/dam/my_dm_sync_folder(/.*/)renditions/original (where my_dm_sync_folder is the folder in AEM with assets synced to Scene7)

Demo | Package Install | Github


Set the Property "Sync to Scene7"




Property set and Saved in CRX

                           Once set, the checkbox is disabled, to avoid the mess of having some assets synced and others remain unsynced





UI Indicator "Uploads to Scene7"



Asset Uploaded to Scene 7




Managed locally in AEM



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-dms7-sync-specific-folders

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-dms7-sync-specific-folders and set a property categories of String[] type to [cq.gui.damadmin.v2.foldershare.coral,dam.gui.admin.coral]dependencies of type String[] with value lodash

3) Create checkbox UI by adding the following code in /apps/eaem-dms7-sync-specific-folders/ui/syncToScene7 of type nt:unstructured

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[cq.gui.damadmin.v2.foldershare.coral,dam.gui.admin.coral]"
dependencies="lodash"/>


4) Create file ( type nt:file ) /apps/eaem-dms7-sync-specific-folders/clientlib/js.txt, add the following

                         add-sync-scene7-checkbox.js

5) Create file ( type nt:file ) /apps/eaem-dms7-sync-specific-folders/clientlib/add-sync-scene7-checkbox.js, add the following code

(function($, $document) {
var FOLDER_SHARE_WIZARD = "/mnt/overlay/dam/gui/content/assets/v2/foldersharewizard.html",
ASSETS_CONSOLE = "/assets.html",
FOUNDATION_CONTENT_LOADED = "foundation-contentloaded",
SEL_DAM_ADMIN_CHILD_PAGES = ".cq-damadmin-admin-childpages",
LAYOUT_CARD_VIEW = "card",
EAEM_SYNC_TO_SCENE7 = "eaemSyncToScene7",
EAEM_UPLOAD_SCENE7_PROPERTY = "eaem-upload-scene7-property",
SYNC_TO_SCENE7_CB = "/apps/eaem-dms7-sync-specific-folders/ui/syncToScene7.html",url = document.location.pathname, $customTab;

if( url.indexOf(FOLDER_SHARE_WIZARD) == 0 ){
handleFolderProperties();
}else if(url.indexOf(ASSETS_CONSOLE) == 0){
$document.on("foundation-selections-change", SEL_DAM_ADMIN_CHILD_PAGES, showSyncUIIndicator);
}

function handleFolderProperties(){
$document.on(FOUNDATION_CONTENT_LOADED, function(){
$.ajax(SYNC_TO_SCENE7_CB).done(addSyncScene7CheckBox);
});
}

function showSyncUIIndicator(e){
if(!e.currentTarget){
return;
}

var $currentTarget = $(e.currentTarget),
foundationLayout = $currentTarget.data("foundation-layout");

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

var layoutId = foundationLayout.layoutId;

if(layoutId !== LAYOUT_CARD_VIEW){
return;
}

var path = $currentTarget.data("foundation-collection-id");

$.ajax(path + ".2.json").done(function(data){
$(".foundation-collection-item").each(function(index, item){
itemHandler(data, layoutId, $(item) );
});
});

function itemHandler(data, layoutId, $item){
var itemPath = $item.data("foundation-collection-item-id"),
itemName = getStringAfterLastSlash(itemPath);

if( (layoutId !== LAYOUT_CARD_VIEW) || !_.isEmpty($item.find("." + EAEM_UPLOAD_SCENE7_PROPERTY))) {
return;
}

if(!data[itemName] || !data[itemName]["jcr:content"] || !data[itemName]["jcr:content"][EAEM_SYNC_TO_SCENE7]){
return;
}

var $cardProperties = $item.find("coral-card-content > coral-card-propertylist");
$cardProperties.append(getScene7PropertyHtml());
}

function getScene7PropertyHtml(){
return '<coral-card-property icon="upload" title="Uploads to Scene7" class="' + EAEM_UPLOAD_SCENE7_PROPERTY + '">' +
'<coral-card-property-content>Uploads to Scene7</coral-card-property-content>' +
'</coral-card-property>';
}

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

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

function addSyncScene7CheckBox(html){
var $tabView = $("coral-tabview");

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

var detailsTab = $tabView[0]._elements.panelStack.querySelector("coral-panel:first-child");

if(!detailsTab){
return;
}

var $cbSyncToScene7 = $(html).appendTo($(detailsTab).find(".aem-assets-foldershare-details-container")),
$form = $("form"),
$submit = $("button[type=submit]");

$.ajax($form.attr("action") + "/jcr:content.json").done(setSyncScene7ContentCB);

$submit.click(handlerFolderPropertiesSubmit);

function setSyncScene7ContentCB(fJcrContent){
var $cb = $cbSyncToScene7.find("[type='checkbox']"),
name = $cb.attr("name");

if(_.isEmpty(fJcrContent[name])){
return;
}

$cb[0].checked = true;

$cbSyncToScene7.attr("disabled", "disabled");
}

function handlerFolderPropertiesSubmit(){
var $cb = $cbSyncToScene7.find("[type='checkbox']"),
data = {};

if(!$cb[0].checked){
return;
}

data[$cb.attr("name")] = $cb.val();

$.post( $form.attr("action") + "/jcr:content", data );
}
}
})(jQuery, jQuery(document));

6) Edit the DAM Update Asset workflow (copy of /libs/settings/workflow/models/dam/update_asset.html created in /conf/global/settings/workflow/models/dam/update_asset.html) to add an OR Split step, move the Scene7 step to branch 1 and add a No Operation step in branch 2



7) Add the following script in branch 1 tab of OR Split; So the images and videos uploaded to folders with property eaemSyncToScene7 are ONLY synced to Scene7)

function check() {
var doSync = false;

try {
var path = workflowData.getPayload().toString();

if (path === null) {
return doSync;
}

path = path.replaceAll("/jcr:content/renditions/original", "");

log.info("Checking if asset should be uploaded to Scene7 - " + path);

var payloadNode = workflowSession.getSession().getNode(path);

if (payloadNode === null) {
return doSync;
}

doSync = payloadNode.getParent().getNode("jcr:content").hasProperty("eaemSyncToScene7");

if (doSync === false) {
log.info("Skipping upload to Scene7 - " + path);
} else {
log.info("Uploading to Scene7 - " + path);
}
} catch (e) {
doSync = false;
log.error(e);
}

return doSync;
}



8) For the branch 2 tab (no operation) add the following script

function check(){
return true;
}

9) Sync the workflow to copy the model from /conf/global/settings/workflow/models/dam/update_asset to /var/workflow/models/dam/update_asset

10) Upload an asset to folder with Scene7 sync allowed, you can see the jcr:content/metadata@dam:scene7ID  in asset metadata, confirming the upload to Scene7


11) Assets in folders that are skipped by OR-split do not have jcr:content/metadata@dam:scene7ID  in asset metadata and are not uploaded to Scene7


AEM 65 - Select One checkbox across Multifield Items in a Composite Multifield

$
0
0

Goal


Make sure only one checkbox is selected in a composite multifield with multiple items added

Demo | Package Install | Github


Set Checkbox Attribute

                Set the attribute granite:class="eaem-mf-dialog-select-one-checkbox" for unique selection



Checkbox Selection



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-multifield-select-one-checkbox

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-multifield-select-one-checkbox/clientlib and set a property categories of String[] type to [cq.authoring.dialog.all]dependencies of type String[] with value lodash

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

                         multifield-checkbox.js

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

(function ($, $document) {
var CHECKBOX_SELECTOR = ".eaem-mf-dialog-select-one-checkbox";

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

function addSelectSingleCheckboxListener(){
$document.on('change', CHECKBOX_SELECTOR, function(e) {
$(CHECKBOX_SELECTOR).not(this).prop('checked', false);
});
}
}(jQuery, jQuery(document), Granite.author));



AEM 65 - Composite Multifield with Radio Group

$
0
0

Goal


Create a composite multifield configuration with radio buttons in multifield items

Demo | Package Install | Github


RadioGroup in Multifield

Dialog XML

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="65 Composite Multi Field Radio"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<products
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
eaem-show-on-collapse="EAEM.showProductName"
fieldLabel="Products">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./products">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<product
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Name of Product"
fieldLabel="Product Name"
name="./product"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
<size
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/radiogroup"
name="./size"
vertical="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<small
jcr:primaryType="nt:unstructured"
checked="{Boolean}true"
text="Small"
value="small"/>
<medium
jcr:primaryType="nt:unstructured"
text="Medium"
value="medium"/>
<large
jcr:primaryType="nt:unstructured"
text="Large"
value="large"/>
</items>
</size>
</items>
</column>
</items>
</field>
</products>
</items>
</column>
</items>
</content>
</jcr:root>

Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-composite-multifield-with-radio

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-composite-multifield-with-radio/clientlib and set a property categories of String[] type to [cq.authoring.dialog.all]dependencies of type String[] with value lodash

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

                         select-radios.js

4) Create file ( type nt:file ) /apps/eaem-touchui-composite-multifield-with-radio/clientlib/select-radios.js, add the following code to fix the issue with radio button select on dialog load

(function ($, $document) {
var COMPOSITE_MULTIFIELD_SELECTOR = "coral-multifield[data-granite-coral-multifield-composite]";

$document.on("foundation-contentloaded", function(e) {
var composites = $(COMPOSITE_MULTIFIELD_SELECTOR, e.target);

composites.each(function() {
Coral.commons.ready(this, function(el) {
selectRadioValue(el);
});
});
});

function selectRadioValue(multifield){
var $multifield = $(multifield),
dataPath = $multifield.closest(".cq-dialog").attr("action"),
mfName = $multifield.attr("data-granite-coral-multifield-name");

dataPath = dataPath + "/" + getStringAfterLastSlash(mfName);

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

function handler(mfData){
multifield.items.getAll().forEach(function(item, i) {
var $mfItem = $(item),
$radio = $mfItem.find('[type="radio"]');

var itemName = getJustMFItemName($radio.attr("name")),
radioName = $radio.attr("name");

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

if(_.isEmpty(mfData[itemName]) || _.isEmpty((mfData[itemName][radioName]))){
return;
}

$radio.filter("[value='" + mfData[itemName][radioName] + "']").prop("checked", "true");
});
}

function getJustMFItemName(itemName){
itemName = itemName.substr(itemName.indexOf(mfName) + mfName.length + 1);

itemName = itemName.substring(0, itemName.indexOf("/"));

return itemName;
}
}

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

return str.substr(str.lastIndexOf("/") + 1);
}
}(jQuery, jQuery(document), Granite.author));




AEM 65 - Touch UI RTE (Rich Text Editor) Dialog Color Picker Plugin

$
0
0

Goal


Touch UI Color Picker Plugin for RTE (Rich Text Editor) Dialog - /libs/cq/gui/components/authoring/dialog/richtext

For a similar extension on 64 check this post 63 check this post, 62 check this post, 61 check this post

For demo purposes, dialog of core text component v2 was modified to add the color picker configuration - /apps/core/wcm/components/text/v2/text/cq:dialog/content/items/tabs/items/properties/items/columns/items/column/items/text/rtePlugins

Demo | Package Install | Github


Plugin Configuration

                                         Add node experience-aem under rtePlugins and set features=* 



                                         Add experience-aem#colorPicker to uiSettings > cui > dialogFullScreen for showing the color palette toolbar icon in full screen




RTE Toolbar Icon




Color Picker in Full Screen


Color applied to text




Text with Color in CRX



Solution


1) Login to CRXDE Lite, add nt:folder /apps/eaem-touchui-dialog-rte-color-picker

2) To show the color picker in a dialog create /apps/eaem-touchui-dialog-rte-color-picker/color-picker-popover of type sling:Folder and /apps/eaem-touchui-dialog-rte-color-picker/color-picker-popover/cq:dialog of type nt:unstructured

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Color Picker"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"
margin="{Boolean}false"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<picker
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/colorfield"
name="./color">
<items jcr:primaryType="nt:unstructured">
<red
jcr:primaryType="nt:unstructured"
name="Red"
value="#FF0000"/>
<green
jcr:primaryType="nt:unstructured"
name="Green"
value="#00FF00"/>
<blue
jcr:primaryType="nt:unstructured"
name="Blue"
value="#0000FF"/>
<black
jcr:primaryType="nt:unstructured"
name="Black"
value="#000000"/>
</items>
</picker>
<add
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/button"
class="coral-Button--primary"
id="EAEM_CP_ADD_COLOR"
text="Add Color"/>
<remove
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/button"
class="coral-Button--warning"
id="EAEM_CP_REMOVE_COLOR"
text="Remove Color"/>
</items>
</column>
</items>
</content>
</jcr:root>


                           
3) Create clientlib (cq:ClientLibraryFolder) /apps/eaem-touchui-dialog-rte-color-picker/clientlib set property categories to cq.authoring.dialog.all and dependencies to [underscore]

4) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/js.txt, add the following content

                                    color-picker.js


5) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/color-picker.js, add the following code

(function($, CUI){
var GROUP = "experience-aem",
COLOR_PICKER_FEATURE = "colorPicker",
TCP_DIALOG = "eaemTouchUIColorPickerDialog",
PICKER_NAME_IN_POPOVER = "color",
REQUESTER = "requester",
PICKER_URL = "/apps/eaem-touchui-dialog-rte-color-picker/color-picker-popover/cq:dialog.html",
url = document.location.pathname;

if( url.indexOf(PICKER_URL) !== 0 ){
addPluginToDefaultUISettings();
addDialogTemplate();
}

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

toString: "EAEMColorPickerDialog",

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

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

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

extend: CUI.rte.plugins.Plugin,

pickerUI: null,

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

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

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

this.pickerUI = tbGenerator.createElement(COLOR_PICKER_FEATURE, this, false, { title: "Color Picker" });
tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.pickerUI, 10);

var groupFeature = GROUP + "#" + COLOR_PICKER_FEATURE;
tbGenerator.registerIcon(groupFeature, "textColor");
},

execute: function (id, value, envOptions) {
if(!isValidSelection()){
return;
}

var context = envOptions.editContext,
selection = CUI.rte.Selection.createProcessingSelection(context),
ek = this.editorKernel,
startNode = selection.startNode;

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

var tag = CUI.rte.Common.getTagInPath(context, startNode, "span"), plugin = this, dialog,
color = $(tag).css("color"),
dm = ek.getDialogManager(),
$container = CUI.rte.UIUtils.getUIContainer($(context.root)),
propConfig = {
'parameters': {
'command': this.pluginId + '#' + COLOR_PICKER_FEATURE
}
};

if(this.eaemColorPickerDialog){
dialog = this.eaemColorPickerDialog;
}else{
dialog = new EAEMColorPickerDialog();

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

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

dialog.$dialog.find("iframe").attr("src", getPickerIFrameUrl(color));

this.eaemColorPickerDialog = dialog;
}

dm.show(dialog);

registerReceiveDataListener(receiveMessage);

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

function getPickerIFrameUrl(color){
var url = PICKER_URL + "?" + REQUESTER + "=" + GROUP;

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

return url;
}

function removeReceiveDataListener(handler) {
if (window.removeEventListener) {
window.removeEventListener("message", handler);
} else if (window.detachEvent) {
window.detachEvent("onmessage", handler);
}
}

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

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

var message = JSON.parse(event.data),
action;

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

action = message.action;

if (action === "submit") {
if (!_.isEmpty(message.data)) {
ek.relayCmd(id, message.data);
}
}else if(action === "remove"){
ek.relayCmd(id);
plugin.eaemColorPickerDialog = null;
}else if(action === "cancel"){
plugin.eaemColorPickerDialog = null;
}

dialog.hide();

removeReceiveDataListener(receiveMessage);
}
},

//to mark the icon selected/deselected
updateState: function(selDef) {
var hasUC = this.editorKernel.queryState(COLOR_PICKER_FEATURE, selDef);

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

CUI.rte.plugins.PluginRegistry.register(GROUP,TouchUIColorPickerPlugin);

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

extend: CUI.rte.commands.Command,

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

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

_getTagObject: function(color) {
return {
"tag": "span",
"attributes": {
"style" : "color: " + color
}
};
},

execute: function (execDef) {
var color = execDef.value ? execDef.value[PICKER_NAME_IN_POPOVER] : undefined,
selection = execDef.selection,
nodeList = execDef.nodeList;

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

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

//if no color value passed, assume delete and remove color
if(_.isEmpty(color)){
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
return;
}

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

//remove existing color before adding new color
if (tags != null) {
nodeList.removeNodesByTag(execDef.editContext, tagObj.tag, undefined, true);
nodeList.commonAncestor = nodeList.nodes[0].dom.parentNode;
}

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

CUI.rte.commands.CommandRegistry.register(COLOR_PICKER_FEATURE, TouchUIColorPickerCmd);

function addPluginToDefaultUISettings(){
var toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.inline.toolbar;
toolbar.splice(3, 0, GROUP + "#" + COLOR_PICKER_FEATURE);

toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.fullscreen.toolbar;
toolbar.splice(3, 0, GROUP + "#" + COLOR_PICKER_FEATURE);
}

function addDialogTemplate(){
var url = PICKER_URL + "?" + REQUESTER + "=" + GROUP;

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

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

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

CUI.rte.templates['dlg-' + TCP_DIALOG] = CUI.rte.Templates['dlg-' + TCP_DIALOG] = Handlebars.compile(html);
}
}(jQuery, window.CUI,jQuery(document)));

(function($, $document){
var SENDER = "experience-aem",
REQUESTER = "requester",
COLOR = "color",
ADD_COLOR_BUT = "#EAEM_CP_ADD_COLOR",
REMOVE_COLOR_BUT = "#EAEM_CP_REMOVE_COLOR";

if(queryParameters()[REQUESTER] !== SENDER ){
return;
}

$(function(){
_.defer(stylePopoverIframe);
});

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

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

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

return result;
}

function stylePopoverIframe(){
var queryParams = queryParameters(),
$dialog = $("coral-dialog");

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

$dialog.css("overflow", "hidden").css("background-color", "#fff");

$dialog[0].open = true;

var $addColor = $dialog.find(ADD_COLOR_BUT),
$removeColor = $dialog.find(REMOVE_COLOR_BUT),
color = queryParameters()[COLOR],
$colorPicker = $document.find("coral-colorinput");

if(!_.isEmpty(color)){
color = decodeURIComponent(color);

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

$colorPicker[0].value = color;
}

adjustHeader($dialog);

$colorPicker.css("margin-bottom", "285px");

$(ADD_COLOR_BUT).css("margin-left", "220px");

$addColor.click(sendDataMessage);

$removeColor.click(sendRemoveMessage);
}

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

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

$header.find(".cq-dialog-cancel").click(function(event){
event.preventDefault();

$dialog.remove();

sendCancelMessage();
});
}

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

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

function sendRemoveMessage(){
var message = {
sender: SENDER,
action: "remove"
};

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

function sendDataMessage(){
var message = {
sender: SENDER,
action: "submit",
data: {}
}, $dialog, color;

$dialog = $(".cq-dialog");

color = $dialog.find("[name='./" + COLOR + "']").val();

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

message.data[COLOR] = color;

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


AEM 65 - Content Copy using Workflow Dialog Participant Step

$
0
0

Goal


A sample workflow with dialog participant step for selecting destination folder from a list of options in dropdown. Subfolders under a specific root folder eg. /content/dam are shown in the dropdown

Demo | Package Install | Github


Dialog Path in Participant Step



Dialog in CRX



Dialog in Complete Work Item



Solution


1) Create a datasource /apps/eaem-content-copy-dialog-pariticipant-step/dialogs/folders-ds/folders-ds.jsp for showing the folders in dropdown. Here the root folder was set to /content/dam, but the logic can be adjusted to return a folder list based on usecase

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

<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="org.apache.sling.api.wrappers.ValueMapDecorator" %>
<%@ page import="com.adobe.granite.ui.components.ds.SimpleDataSource" %>
<%@ page import="org.apache.commons.collections.iterators.TransformIterator" %>
<%@ page import="org.apache.commons.collections.Transformer" %>
<%@ page import="org.apache.sling.api.resource.*" %>
<%@ page import="java.util.*" %>

<%
String ROOT_PATH = "/content/dam";

final ResourceResolver resolver = resourceResolver;

Resource rootPath = resolver.getResource(ROOT_PATH);

List<Resource> qualifiedParents = new ArrayList<Resource>();

rootPath.listChildren().forEachRemaining(r -> {
if(r.getName().equals("jcr:content")){
return;
}

qualifiedParents.add(r);
});

DataSource ds = new SimpleDataSource(new TransformIterator(qualifiedParents.iterator(), new Transformer() {
public Object transform(Object o) {
Resource resource = (Resource) o;
ValueMap vm = new ValueMapDecorator(new HashMap<String, Object>()),
tvm = resource.getValueMap();

vm.put("value", resource.getPath());
vm.put("text", tvm.get("jcr:content/jcr:title", tvm.get("jcr:title", resource.getName())));

return new ValueMapResource(resolver, new ResourceMetadata(), "nt:unstructured", vm);
}
}));

request.setAttribute(DataSource.class.getName(), ds);
%>

2) Create the dialog /apps/eaem-content-copy-dialog-pariticipant-step/dialogs/content-copy-dialog to show in Inbox console work item Complete Workflow Item

<?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"
jcr:title="Experience AEM Content Copy"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<parent
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldDescription="Select Parent Folder"
fieldLabel="Parent Folder"
name="parentFolder">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/eaem-content-copy-dialog-pariticipant-step/dialogs/folders-ds"
addNone="{Boolean}true"/>
</parent>
</items>
</column>
</items>
</content>
</jcr:root>


3) Add the dialog path in Dialog Participant Step of workflow model e.g. /conf/global/settings/workflow/models/experience-aem-content-copy



4) Create a process step e.g. apps.experienceaem.assets.ContentCopyTask to read the parent folder path set in parentFolder of previous step (dialog drop down)

package apps.experienceaem.assets;

import com.adobe.granite.workflow.PayloadMap;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;

@Component(
immediate = true,
service = {WorkflowProcess.class},
property = {
"process.label = Experience AEM Content Copy Task"
}
)
public class ContentCopyTask implements WorkflowProcess {
protected final Logger log = LoggerFactory.getLogger(this.getClass());

private static final String DIALOG_PARTICIPANT_NODE_ID = "node1";
private static final String DESTINATION_PATH = "parentFolder";

@Reference
private ResourceResolverFactory resolverFactory;

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

Asset payload = getAssetFromPayload(item, resolver);

if(payload == null){
log.error("Empty payload, nothing to copy");
return;
}

String destinationPath = getDestinationPathForCopy(item, resolver);

if(StringUtils.isEmpty(destinationPath)){
log.error("Destination path empty for copyign content - " + payload.getPath());
return;
}

Node copiedPath = JcrUtil.copy(payload.adaptTo(Node.class), resolver.getResource(destinationPath).adaptTo(Node.class), null);

log.debug("Copied Path - " + copiedPath);

session.save();
} catch (Exception e) {
log.error("Failed to copy content", e);
}
}

private String getDestinationPathForCopy(WorkItem item, ResourceResolver resolver) throws Exception{
String wfHistoryPath = item.getWorkflow().getId() + "/history";

Iterator<Resource> historyItr = resolver.getResource(wfHistoryPath).listChildren();
ValueMap metaVM = null;

Resource resource, itemResource;
String nodeId, destinationPath = "";

while(historyItr.hasNext()){
resource = historyItr.next();

itemResource = resource.getChild("workItem");

nodeId = itemResource.getValueMap().get("nodeId", "");

if(!nodeId.equals(DIALOG_PARTICIPANT_NODE_ID)){
continue;
}

metaVM = itemResource.getChild("metaData").getValueMap();

destinationPath = metaVM.get(DESTINATION_PATH, "");

break;
}

return destinationPath;
}

public Asset getAssetFromPayload(final WorkItem item, final ResourceResolver resolver) throws Exception{
Asset asset = null;

if (!item.getWorkflowData().getPayloadType().equals(PayloadMap.TYPE_JCR_PATH)) {
return null;
}

final String path = item.getWorkflowData().getPayload().toString();
final Resource resource = resolver.getResource(path);

if (null != resource) {
asset = DamUtil.resolveToAsset(resource);
} else {
log.error("getAssetFromPayload: asset [{}] in payload of workflow [{}] does not exist.", path,
item.getWorkflow().getId());
}

return asset;
}

private ResourceResolver getResourceResolver(final Session session) throws LoginException {
return resolverFactory.getResourceResolver( Collections.<String, Object>
singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session));
}
}




AEM 65 - Touch UI RTE (Rich Text Editor) Plugin for Creating Structured Content e.g. Creating Tooltips

$
0
0

Goal


Create a Touch UI RTE (Rich Text Editor) plugin for entering Structured Content, converted into HTML and added in RTE based on required functionality

With the solution discussed in this post, author can enter content in a form opened from RTE, convert into HTML, add as a tooltip for selected text

Demo | Package Install | Github


Plugin Configuration

                               Add node experience-aem under rtePlugins and set features=* 




                               Add experience-aem#structuredContentModal to uiSettings> cui > dialogFullScreen for showing the ellipsis toolbar icon in full screen



RTE Toolbar Icon




Content Form (for Tooltip) in Full Screen


Tooltip applied to text




Tooltip content in CRX



Solution


1) Login to CRXDE Lite, add nt:folder /apps/eaem-touchui-rte-structured-content

2) To show the tooltip form on plugin icon click, create /apps/eaem-touchui-rte-structured-content/structured-content of type cq:Page (line 19, check the clientlib category eaem.rte.structured.content.plugin)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="cq:Page">
<jcr:content
jcr:mixinTypes="[sling:VanityPath]"
jcr:primaryType="nt:unstructured"
jcr:title="Experience AEM Structured Content"
sling:resourceType="granite/ui/components/coral/foundation/page">
<head jcr:primaryType="nt:unstructured">
<favicon
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
<viewport
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
<clientlibs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral,eaem.rte.structured.content.plugin]"/>
</head>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/body">
<items jcr:primaryType="nt:unstructured">
<form
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form"
class="foundation-form content-container"
maximized="{Boolean}true"
style="vertical">
<items jcr:primaryType="nt:unstructured">
<wizard
jcr:primaryType="nt:unstructured"
jcr:title="Add tooltip content..."
sling:resourceType="granite/ui/components/coral/foundation/wizard">
<items jcr:primaryType="nt:unstructured">
<tooltip
jcr:primaryType="nt:unstructured"
jcr:title="Tooltip"
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
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Enter Title"
fieldLabel="Title"
name="title"/>
<description
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
fieldDescription="Enter Description"
fieldLabel="Description"
name="description"/>
</items>
</column>
</items>
</columns>
</items>
</tooltip>
<parentConfig jcr:primaryType="nt:unstructured">
<prev
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
text="Cancel">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="cancel"/>
</prev>
<next
granite:class="foundation-wizard-control"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/button"
disabled="{Boolean}true"
text="Create tooltip"
type="submit"
variant="primary">
<granite:data
jcr:primaryType="nt:unstructured"
foundation-wizard-control-action="next"/>
</next>
</parentConfig>
</items>
</wizard>
</items>
</form>
</items>
</body>
</jcr:content>
</jcr:root>




3) Create clientlib (cq:ClientLibraryFolder) /apps/eaem-touchui-dialog-rte-color-picker/clientlib set property categories to [cq.authoring.dialog.all, eaem.rte.structured.content.plugin] and dependencies to [lodash]

4) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/css.txt, add the following content

                                    rte-structured-content.css


5) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/rte-structured-content.css, add the following code

.eaem-rte-structured-dialog {
width: 80%;
margin-left: -20%;
height: 83%;
margin-top: -20%;
box-sizing: content-box;
z-index: 10100;
}

.eaem-rte-structured-dialog > iframe {
width: 100%;
height: 100%;
border: 1px solid #888;
}


6) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/js.txt, add the following content

                                    rte-structured-content.js


7) Create file (nt:file) /apps/eaem-touchui-dialog-rte-color-picker/clientlib/rte-structured-content.js, add the following code. #25 function getHtmlFromContent() converts the form content into tooltip html and adds it for selected text. Logic in this function can be adjusted accordingly, to create HTML from form content and add in RTE

(function($, CUI, $document){
var GROUP = "experience-aem",
STRUCTURED_CONTENT_FEATURE = "structuredContentModal",
TCP_DIALOG = "eaemTouchUIStructuredContentModalDialog",
CONTENT_IN_DIALOG = "content",
REQUESTER = "requester",
CANCEL_CSS = "[data-foundation-wizard-control-action='cancel']",
MODAL_URL = "/apps/eaem-touchui-rte-structured-content/structured-content.html",
$eaemStructuredModal, url = document.location.pathname;

if( url.indexOf(MODAL_URL) !== 0 ){
addPluginToDefaultUISettings();

addDialogTemplate();

addPlugin();
}else{
$document.on("foundation-contentloaded", fillDefaultValues);

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

$document.submit(sendTextAttributes);
}

function getHtmlFromContent(selectedText, content){
var tooltipText = content.title + " : " + content.description;

return "<span title='" + tooltipText + "' class='eaem-dotted-underline' data-content='" + JSON.stringify(content) + "'>" +
selectedText +
"</span>";
}

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

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

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

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

return result;
}

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

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

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

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

if(_.isEmpty(queryParams[CONTENT_IN_DIALOG])){
return;
}

var content = JSON.parse(decodeURIComponent(queryParams[CONTENT_IN_DIALOG]));

_.each(content, function(value, key){
setWidgetValue(form, "[name='" + key + "']", value);
});
}

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

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

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

return parent;
}

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

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

var message, action;

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

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

action = message.action;

if(action === "submit"){
var ek = $eaemStructuredModal.eaemModalPlugin.editorKernel,
tooltipHtml = getHtmlFromContent(window.getSelection().toString(), message.data);

ek.execCmd('inserthtml', tooltipHtml);

ek.focus();
}

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

function addPluginToDefaultUISettings(){
var toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.inline.toolbar;
toolbar.splice(3, 0, GROUP + "#" + STRUCTURED_CONTENT_FEATURE);

toolbar = CUI.rte.ui.cui.DEFAULT_UI_SETTINGS.fullscreen.toolbar;
toolbar.splice(3, 0, GROUP + "#" + STRUCTURED_CONTENT_FEATURE);
}

function addDialogTemplate(){
var url = MODAL_URL + "?" + REQUESTER + "=" + GROUP;

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

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

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

CUI.rte.templates['dlg-' + TCP_DIALOG] = CUI.rte.Templates['dlg-' + TCP_DIALOG] = Handlebars.compile(html);
}

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

extend: CUI.rte.plugins.Plugin,

modalUI: null,

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

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

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

this.modalUI = tbGenerator.createElement(STRUCTURED_CONTENT_FEATURE, this, false, { title: "Add tooltip" });
tbGenerator.addElement(GROUP, plg.Plugin.SORT_FORMAT, this.modalUI, 10);

var groupFeature = GROUP + "#" + STRUCTURED_CONTENT_FEATURE;
tbGenerator.registerIcon(groupFeature, "more");

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

execute: function (id, value, envOptions) {
if(!isValidSelection()){
return;
}

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

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

var tag = CUI.rte.Common.getTagInPath(context, startNode, "span"), plugin = this, dialog,
content = $(tag).data("content");

this.showDialogModal(getModalIFrameUrl(content));

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

function getModalIFrameUrl(content){
var url = MODAL_URL + "?" + REQUESTER + "=" + GROUP;

if(_.isObject(content)){
url = url + "&" + CONTENT_IN_DIALOG + "=" + JSON.stringify(content);
}

return url;
}
},

showDialogModal: function(url){
var self = this, $iframe = $('<iframe>'),
$modal = $('<div>').addClass('eaem-rte-structured-dialog coral-Modal');

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

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

$eaemStructuredModal = $modal;

$eaemStructuredModal.eaemModalPlugin = self;

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

//to mark the icon selected/deselected
updateState: function(selDef) {
var hasUC = this.editorKernel.queryState(STRUCTURED_CONTENT_FEATURE, selDef);

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

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



AEM 64 - Creating Dynamic Brand specific Emails using Experience Fragments for Delivery using Adobe Campaign

$
0
0

Goal


Leveraging Experience Fragments (XF) in AEM and AEM - ACS (Adobe Campaign Standard) integration, create Brand specific emails with dynamic personalization based on the recipient's profile attributes...

Here the usecase is say, sending About Your State emails to profiles (managed in Adobe Campaign) on Independence Day; assuming the profiles in campaign have Address> State attribute entered

Demo | Package Install | Github


Solution


Following are the high level steps explaining creation of Dynamic Brand (here State) Specific Emails

1) Content authors specific to the territory (state) create state specific experience fragments in AEM e.g. Texas XF, California XF and tag it with respective state

2) Content approver creates the email by adding Experience AEM Dynamic EMail Experience Fragment Component, on AEM page of template type Adobe Campaign Email (ACS), selecting the campaign folder e.g. Summer 2019, starts and completes the workflow Approve for Adobe Campaign

3) On the ACS side, campaign manager creates a template for importing the AEM content with experience fragments and conditional expressions

4) Create an email selecting the target audience using Profile attribute State (stateLink)

5) Prepare and Send emails


Create Location Tags

Setup the Tag Structure in AEM. Identifying the right content (state specific) for a recipient's email is based on the tag its associated to...

e.g. Here adding the tag TX to an experience fragment sends it to Texas recipients




Create Experience Fragments

Enable the content authors to create state specific experience fragments...

1) Create an experience fragment of template type /libs/cq/experience-fragments/components/experiencefragment/template for Texas state with texas flag in a Image component and some write up about texas in a Text component e.g. /content/experience-fragments/summer_2019/texas-xf/master.html and tag it with /content/cq:tags/locations/TX






2) Similarly create the California XF /content/experience-fragments/summer_2019/california-xf/master.html tag it with /content/cq:tags/locations/CA




3) The experience fragments storage structure /content/experience-fragments/summer_2019




Create the Email Page

1) Assign the AEM - ACS cloud configuration to brand Experience AEM/content/campaigns/experience-aem




2) Create the Campaign page structure and page /content/campaigns/experience-aem/main-area/independence-day/about-states-email of template type /libs/mcm/campaign/templates/ac-email-acs



3) Drag and drop the component /apps/eaem-campaign-dynamic-emails-component/components/campaign-dynamic-email-xf-component and select path of the folder with campaign specific XFs (all XFs in this folder are tagged with their respective states TX, CA etc)



4) The component reads XFs, renders them and adds the necessary conditional expressions with context variables. we are interested in context variable context.profile.location.stateCode for the expression (read by campaign later during email delivery)

                     /apps/eaem-campaign-dynamic-emails-component/components/campaign-dynamic-email-xf-component/campaign-dynamic-email-xf-component.html

<div data-sly-use.dynamicEmail="DynamicEmailUsePojo">
<div data-sly-test="${!dynamicEmail.regionContent}" style="padding:10px ; background-color: #3e999f">
Select campaign folder with region specific experience fragments...
</div>
<div data-sly-test="${dynamicEmail.regionContent}">
<P>Hi <%= context.profile.lastName %>!</P>

<div data-sly-list.stateCode="${dynamicEmail.regionContent}">
<div>
${dynamicEmail.conditionalExpressionBegin @ context='unsafe'} if ( context.profile.location.stateCode == '${stateCode}' ) { ${dynamicEmail.conditionalExpressionEnd @ context='unsafe'}

<sly data-sly-resource="${@path=dynamicEmail.regionContent[stateCode], selectors='content', wcmmode='disabled'}"></sly>

<% } %>
</div>
</div>
</div>
</div>

<div data-sly-test="${!wcmmode.disabled && !personalization.isTouch}"
data-sly-use.clientLib="${'/libs/granite/sightly/templates/clientlib.html'}" data-sly-unwrap>
<meta data-sly-call="${clientLib.all @ categories='cq.personalization'}" data-sly-unwrap></meta>
<script type="text/javascript">
CQ.personalization.variables.Variables.applyToEditComponent("${resource.path @ context='scriptString'}");
</script>
</div>


                     /apps/eaem-campaign-dynamic-emails-component/components/campaign-dynamic-email-xf-component/DynamicEmailUsePojo.java

package apps.eaem_campaign_dynamic_emails_component.components.campaign_dynamic_email_xf_component;

import com.day.cq.tagging.Tag;
import com.day.cq.wcm.api.Page;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.sightly.WCMUsePojo;

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

public class DynamicEmailUsePojo extends WCMUsePojo {

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

private static String CAMPAIGN_FOLDER_PATH = "campaignFolderPath";
private static String EMAIL_BODY = "emailBody";
private static String LOCATIONS_NAME_SPACE = "locations";


private Map<String, String> regionContent = new HashMap<String, String>();

@Override
public void activate() {
String xfFolderPath = getProperties().get(CAMPAIGN_FOLDER_PATH, "");

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

ResourceResolver resolver = getResourceResolver();

Resource xfFolder = resolver.getResource(xfFolderPath);

if(xfFolder == null){
return;
}

Iterator<Resource> xfFolderItr = xfFolder.listChildren();
Page xfMaster = null;
Tag[] tags = null;

while(xfFolderItr.hasNext()){
xfMaster = xfFolderItr.next().getChild("master").adaptTo(Page.class);

tags = xfMaster.getTags();

if(ArrayUtils.isEmpty(tags)) {
continue;
}

for(Tag tag : tags){
if(!tag.getNamespace().getName().equals(LOCATIONS_NAME_SPACE)){
continue;
}

regionContent.put(tag.getName(), xfMaster.getPath() + "/jcr:content");
}
}
}

public Map<String, String> getRegionContent() {
return regionContent;
}

public String getConditionalExpressionBegin(){
return "<% ";
}

public String getConditionalExpressionEnd(){
return " %>";
}
}


5) Run the workflow Approve for Adobe Campaign (make it available for ACS)




6) Since the images displayed in email are directly accessed from AEM Publish, publish the images added in XF /content/dam/experience-aem (this can be streamlined into a workflow step)


Setup the Delivery Template in ACS

1) Navigate to Adobe Campaign using Experience cloud solution switcher and access Delivery templates - Adobe Campaign Logo > Resources > Templates > Delivery Templates

2) Duplicate the template AEM Delivery Template and create States AEM Template for this campaign specific emails...



3) Edit the new template created States AEM Template, and import the email page content created in AEM - /content/campaigns/experience-aem/main-area/independence-day/about-states-email




Create & Send Brand (State) Specific Email

1) From Campaign Home click on Create an Email



2) Select the template States AEM Template created in previous section...



3) Enter email properties



4) In this step, select the profile attributes for targeting & personalization. Here we drag and drop the attribute Location> State (stateLink) and add rules, to pick the recipients belonging to Texas and California




5) Click on Prepare to run the prechecks, find number of deliveries etc.





6) Email sent statistics...



7) Profiles with state TX in address receive Texas specific content (added in Texas tagged experience fragment in AEM)




8) Profiles with state CA in address receive California specific content (added in California specific experience fragment)






AEM 65 - Touch UI Filter Column View Items in PathField (Autocomplete) Picker

$
0
0

Goal


Filter the results in picker's column view of PathField (granite/ui/components/coral/foundation/form/pathfield) to show assets with the metadata dc:creator entered (a sample to show assets based on required metadata )

This extension filters the picker's column view items only and not search results in PathField

Demo | Package Install | Github


Set the Creator (dc:creator)



Assets with dc:creator shown in PathField



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-pathfield-filter-results

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-pathfield-filter-results/clientlib and set a property categories of String[] type to [cq.authoring.dialog.all]dependencies of type String[] with value lodash

3) Create file ( type nt:file ) /apps/eaem-touchui-pathfield-filter-results/clientlib/js.txt, add the following

                         pathfield-filter.js

4) Create file ( type nt:file ) /apps/eaem-touchui-pathfield-filter-results/clientlib/pathfield-filter.js, add the following code

(function ($, $document) {
var LINK_URL = "./linkURL",
COMPONENT = "weretail/components/content/image";

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

function handlePathField(){
var $linkUrl = $("foundation-autocomplete[name='" + LINK_URL + "']"),
gAuthor = Granite.author,
editable = gAuthor.DialogFrame.currentDialog.editable;

//if not an weretail image component dialog, return
if((editable.type !== COMPONENT) || _.isEmpty($linkUrl)){
return;
}

var pathField = $linkUrl[0];

extendPicker(pathField);
}

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

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

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

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

function filterContent(event){
var $item, currentPath = $(event.detail.activeItem).data("foundationCollectionItemId");

$.ajax(currentPath + ".3.json").done(handler);

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

if(isValid(assetsJson, $item)){
return;
}

$item.remove();
});
}
}

function isValid(assetsJson, $item){
var itemPath = $item.data("foundationCollectionItemId"),
itemName = itemPath.substring(itemPath.lastIndexOf("/") + 1),
assetMetadata = assetsJson[itemName];

if($item.attr("variant") == "drilldown"){ // a folder
return true;
}

if(!assetMetadata || !assetMetadata["jcr:content"] || !assetMetadata["jcr:content"]["metadata"]
|| !assetMetadata["jcr:content"]["metadata"]["dc:creator"]){
return false;
}

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

AEM 65 - Extend Inbox List view and Add Column Path

$
0
0

Goal


Extend the List view of Inbox (/aem/inbox,/mnt/overlay/cq/inbox/content/inbox.html)  and add columnPath

Demo | Package Install | Github


Product



Extension



Solution


1) Login to CRXDE Lite, create folder (nt:folder) /apps/eaem-touchui-inbox-add-column-folder

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-touchui-inbox-add-column-folder/clientlib and set a property categories of String[] type to [cq.inbox.gui]dependencies of type String[] with value lodash

3) Create file ( type nt:file ) /apps/eaem-touchui-inbox-add-column-folder/clientlib/js.txt, add the following

                         add-column.js

4) Create file ( type nt:file ) /apps/eaem-touchui-inbox-add-column-folder/clientlib/add-column.js, add the following code

(function($, $document){
var _ = window._,
INBOX_UI_PAGE_VANITY = "/aem/inbox",
INBOX_UI_PAGE = "/mnt/overlay/cq/inbox/content/inbox.html",
LAYOUT_LIST_VIEW = "list",
columnAdded = false;

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

function isInboxPage() {
var pathName = window.location.pathname;
return ((pathName.indexOf(INBOX_UI_PAGE) >= 0) || (pathName.indexOf(INBOX_UI_PAGE_VANITY) >= 0));
}

function addColumn(){
if(!isInboxPage() || columnAdded){
return;
}

var $thead = $("thead");

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

columnAdded = true;

$thead.find("tr").append(getFolderColumnHeader());

addContent();
}

function addContent(){
var $table = $("table"),
foundationLayout = $table.data("foundation-layout");

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

var layoutId = foundationLayout.layoutId;

if(layoutId !== LAYOUT_LIST_VIEW){
return;
}

$(".foundation-collection-item").each(function(index, item){
itemHandler($(item) );
});
}

function itemHandler($item){
var payLoadLink = $item.data("payload-link");

$item.append(getListCellHtml(_.isEmpty(payLoadLink) ? "" : payLoadLink.substring(payLoadLink.indexOf("/content"))));
}

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

function getFolderColumnHeader(){
return '<th is="coral-table-headercell">Path</th>';
}
}(jQuery, jQuery(document)));

AEM 65 - TouchUI Composite Multifield with Coral3 RTE (RichText) Field

$
0
0

Goal


Create a sample Composite Multifield configuration with Rich Text Editor widget cq/gui/components/authoring/dialog/richtext , read and render data on page using HTL (Sightly) script

Demo | Package Install | Github


RTE Configuration



RTE in Multifield



Data Saved in CRX



Solution


1) Login to CRXDe Lite http://localhost:4502/crx/de/index.jsp and create the RTE field configuration in component cq:dialog e.g. /apps/eaem-touchui-rte-composite-multifield/sample-rte-composite-multifield/cq:dialog/content/items/column/items/products/field/items/column/items/text

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="65 RTE Composite Multi Field"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<products
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Products">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./products">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<product
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Name of Product"
fieldLabel="Product Name"
name="./product"/>
<path
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldDescription="Select Path"
fieldLabel="Path"
name="./path"
rootPath="/content"/>
<text
jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
name="./text"
useFixedInlineToolbar="{Boolean}true">
<rtePlugins jcr:primaryType="nt:unstructured">
<format
jcr:primaryType="nt:unstructured"
features="bold,italic"/>
<justify
jcr:primaryType="nt:unstructured"
features="-"/>
<links
jcr:primaryType="nt:unstructured"
features="modifylink,unlink"/>
<lists
jcr:primaryType="nt:unstructured"
features="*"/>
<paraformat
jcr:primaryType="nt:unstructured"
features="*">
<formats jcr:primaryType="nt:unstructured">
<default_p
jcr:primaryType="nt:unstructured"
description="Paragraph"
tag="p"/>
<default_h1
jcr:primaryType="nt:unstructured"
description="Heading 1"
tag="h1"/>
<default_h2
jcr:primaryType="nt:unstructured"
description="Heading 2"
tag="h2"/>
<default_h3
jcr:primaryType="nt:unstructured"
description="Heading 3"
tag="h3"/>
</formats>
</paraformat>
<tracklinks
jcr:primaryType="nt:unstructured"
features="*"/>
</rtePlugins>
<uiSettings jcr:primaryType="nt:unstructured">
<cui jcr:primaryType="nt:unstructured">
<inline
jcr:primaryType="nt:unstructured"
toolbar="[format#bold,format#italic,format#underline,#justify,#lists,links#modifylink,links#unlink,#paraformat]">
<popovers jcr:primaryType="nt:unstructured">
<justify
jcr:primaryType="nt:unstructured"
items="[justify#justifyleft,justify#justifycenter,justify#justifyright]"
ref="justify"/>
<lists
jcr:primaryType="nt:unstructured"
items="[lists#unordered,lists#ordered,lists#outdent,lists#indent]"
ref="lists"/>
<paraformat
jcr:primaryType="nt:unstructured"
items="paraformat:getFormats:paraformat-pulldown"
ref="paraformat"/>
</popovers>
</inline>
<dialogFullScreen
jcr:primaryType="nt:unstructured"
toolbar="[format#bold,format#italic,format#underline,justify#justifyleft,justify#justifycenter,justify#justifyright,lists#unordered,lists#ordered,lists#outdent,lists#indent,links#modifylink,links#unlink,table#createoredit,#paraformat,image#imageProps]">
<popovers jcr:primaryType="nt:unstructured">
<paraformat
jcr:primaryType="nt:unstructured"
items="paraformat:getFormats:paraformat-pulldown"
ref="paraformat"/>
</popovers>
</dialogFullScreen>
</cui>
</uiSettings>
</text>
</items>
</column>
</items>
</field>
</products>
</items>
</column>
</items>
</content>
</jcr:root>


2) Create file (nt:file) /apps/eaem-touchui-rte-composite-multifield/sample-rte-composite-multifield/helper.js, add the following code for reading entered data (stored in nodes)
"use strict";

use( ["/libs/wcm/foundation/components/utils/ResourceUtils.js","/libs/sightly/js/3rd-party/q.js" ], function(ResourceUtils, Q){
var prodPromise = Q.defer(), company = {},
productsPath = granite.resource.path + "/products";

company.products = undefined;

ResourceUtils.getResource(productsPath)
.then(function (prodParent) {
return prodParent.getChildren();
})
.then(function(products) {
addProduct(products, 0);
});

function addProduct(products, currIndex){
if(!company.products){
company.products = [];
}

if (currIndex >= products.length) {
prodPromise.resolve(company);
return;
}

var productRes = products[currIndex],
properties = productRes.properties;

var product = {
path: properties.path,
name: properties.product,
text: properties.text
};

log.info("----" + product.text);

company.products.push(product);

addProduct(products, (currIndex + 1));
}

return prodPromise.promise;
} );

3) Create file (nt:file) /apps/eaem-touchui-rte-composite-multifield/sample-rte-composite-multifield/sample-rte-composite-multifield.html, add the following HTL code for displaying MF entered data on page

<div>
<b>65 RTE Composite Multifield</b>

<div data-sly-use.company="helper.js" data-sly-unwrap>
<div data-sly-test="${!company.products && wcmmode.edit}">
Add products using component dialog
</div>

<div data-sly-test="${company.products}">
<div data-sly-list.product="${company.products}">
<div>
<div><strong>${product.name}</strong></div>
<div>${product.path}</div>
<div>${product.text @ context='html'}</div>
</div>
</div>
</div>
</div>
</div>



AEM 65 - Extend Asset Finder, add Dynamic Media Scene7 component for Audio and Video

$
0
0

Goal


Extend the authoring Asset Finder to show EAEM Audio option in Asset filter dropdown

Extend the Dynamic Media Scene7 component /libs/dam/components/scene7/dynamicmedia and create a new component /apps/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component to play both Audio and Video files (product component is for videos only)

For configuring Dynamic Media Scene7 check this post

Demo | Package Install | Github


Asset Finder extension for Audio



Playing Video



Solution


1) To add the EAEM Audio filter in Asset Finder, login to CRXDE Lite, create folder (nt:folder) /apps/eaem-dms7-audio-video-component

2) Create clientlib (type cq:ClientLibraryFolder) /apps/eaem-dms7-audio-video-component/clientlib and set a property categories of String[] type to [cq.authoring.editor.hook.assetfinder]dependencies of type String[] with value lodash

3) Create file ( type nt:file ) /apps/eaem-dms7-audio-video-component/clientlib/js.txt, add the following

                         assetfinder-audio.js

4) Create file ( type nt:file ) /apps/eaem-dms7-audio-video-component/clientlib/assetfinder-audio.js, add the following code

(function ($, $document, author) {
var self = {},
EAEM_AUDIO = 'EAEM Audio';

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

self.loadAssets = function (query, lowerLimit, upperLimit) {
var param = {
'_dc': new Date().getTime(),
'query': query.concat("order:\"-jcr:content/jcr:lastModified\""),
'mimeType': 'audio',
'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(EAEM_AUDIO, self);
}(jQuery, jQuery(document), Granite.author));


5) To support both audio and video in same component extend otb Dynamic Media Scene7 component /libs/dam/components/scene7/dynamicmedia and create component /apps/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component

6) Create node /apps/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component/cq:editConfig of type cq:EditConfig with cq:dropTargets/image@accept=[video/*,audio/*] to support drag and drop of assets of type audio & video onto the component



7) Create use api javascript /apps/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component/audio-video.js, add the following code to read asset specific metadata

use( function(){
var METADATA_NODE = com.day.cq.dam.api.s7dam.constants.S7damConstants.S7_ASSET_METADATA_NODE,
SCENE7_FODLER = METADATA_NODE + "/metadata/dam:scene7Folder",
SCENE7_DOMAIN = METADATA_NODE + "/metadata/dam:scene7Domain",
fileReference = properties['./fileReference'],
rootNode = currentSession.rootNode,
mediaInfo = {};

if(!fileReference) {
mediaInfo.isAudio = false;
return;
}

var assetNode = rootNode.getNode(fileReference.substring(1)),
publishUrl = getPublishServerURL(assetNode);

//log.info("publishUrl----------" + publishUrl);

return{
isAudio: isAudioFile(assetNode),
assetName: assetNode.getName(),
s7PublishPath: publishUrl,
bgImage: assetNode.getPath() + "/jcr:content/renditions/poster.png"
};

function isAudioFile(assetNode){
var META_DC_FORMAT = "jcr:content/metadata/dc:format",
isAudioFile = false;

if(assetNode == undefined){
return isAudioFile;
}

if( assetNode.hasProperty(META_DC_FORMAT)) {
var dcFormat = assetNode.getProperty(META_DC_FORMAT).getString();
isAudioFile = dcFormat.startsWith("audio");
}

return isAudioFile;
}

function getPublishServerURL(assetNode) {
var s7Path = assetNode.getProperty(SCENE7_DOMAIN).getString() + "is/content/"
+ assetNode.getProperty(SCENE7_FODLER).getString() + assetNode.getName();

return s7Path;
}
});


8) For audio files, the above script assumes a rendition with name poster.png exists for audio files to shows as static background for audio player



9) For audio files, you can either publish the audio assets to play in component (accessed from Scene7 delivery server) or add more logic to read the secure preview server url from dynamic media configuration to play in author AEM and use S7 delivery server url for publish AEM e.g.

              S7 Secure Preview server url (audio not published) - https://preview1.assetsadobe.com/is/content/CEM/experience-aem-65/muzik.mp3
              S7 Delivery server url (audio published) - http://s7d9.scene7.com/is/content/CEM/experience-aem-65/muzik.mp3



10) Add HTL script /apps/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component/eaem-dms7-audio-video-component.html to play the audio files using<audio> tag and video files using S7 player

<style>
.eaem-dms7-audio-container {
position: relative;
text-align: center;
}

.eaem-dms7-audio-centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

<div data-sly-use.mediaInfo="audio-video.js" style="padding:10px">
<h2>This component can play both audio and video</h2>
<div data-sly-test="${mediaInfo.isAudio}">
<h2 data-sly-test="${mediaInfo.assetName}">Currently playing audio : ${mediaInfo.assetName}</h2>
<div class="eaem-dms7-audio-container">
<img class="eaem-dms7-audio-image" src="${mediaInfo.bgImage}">
<div class="eaem-dms7-audio-centered">
<audio class="eaem-dms7-audio-audio" width="100%" height="100%" preload="auto" controls="controls">
<source src="${mediaInfo.s7PublishPath}" type="audio/ogg">
</audio>
</div>
</div>
</div>
<div data-sly-test="${!mediaInfo.isAudio}" data-sly-use.mediaInfo="dynamicmedia_sly.js">
<h2 data-sly-test="${mediaInfo.assetName}">Currently playing video : ${mediaInfo.assetName}</h2>
<div data-sly-include="/libs/dam/components/scene7/dynamicmedia/dynamicmedia.html"></div>
</div>
</div>


AEM 65 - Configure InDesign Server Job Options in Folder Metadata for PDF Generation using Presets

$
0
0

Goal


For InDesign files uploaded to AEM, specify Folder specific PDF Presets and generate PDFs using AEM InDesign Server integration. Add IDS Job Options file name in AEM folder metadata, read and use in the PDF generation script executed in InDesign Server. A Job Options file is an Adobe PDF Preset file that defines the properties of a PDF generated using applications like InDesign.

This extension is for configuring the PDF presets per AEM folder. Here a folder in AEM is configured with preset EAEM Gray and the generated PDFs uploaded to that folder by InDesign Server have all colors converted to gray

About AEM folder metadata schemas check documentation here

About InDesign PDF Job Options check documentation here

Demo | Package Install | Github


PDF Generated by Product



PDF with Preset [EAEM Gray] generated by Extension



Solution


1) Create the specific PDF preset EAEM Gray using Adobe InDesign 2019 CC



2) Job options file for the preset - EAEM Gray.joboptions (download here) gets created in folder C:\Users\<user>\AppData\Roaming\Adobe\Adobe PDF\Settings. Following properties in the job options file define color conversion...

                                  /ConvertColors /ConvertToGray
                                  /DestinationProfileName (Dot Gain 15%)

3) Copy the file to InDesign server PDF Presets location e.g. C:\Program Files\Adobe\Adobe InDesign CC Server 2019\Resources\Adobe PDF\settings\mul

Similarly copy all necessary PDF presets joboptions files into this location (only the name of joboptions file is configured in AEM folder metadata, actual file resides in InDesign Server)



4) Create a folder metadata schema Experience AEM InDesign for configuring the preset name EAEM Gray and apply to a folder in AEM e.g. /content/dam/experience-aem



5) Create InDesign script PDFExportUsingJobOptions.jsx with the following code, for reading preset name in AEM folder metadata using query builder, generate the PDF,  upload to AEM (create the script in location /apps/settings/dam/indesign/scripts and make sure user idsjobprocessor has read permissions on the folder)

app.consoleout('START - Creating PDF Export using jobOptions....');

exportPDFUsingJobOptions();

app.consoleout('END - Creating PDF Export using jobOptions....');

function exportPDFUsingJobOptions(){
var QUERY = "/bin/querybuilder.json?path=/content/dam&nodename=" + getAEMNodeName(),
META_EAEM_PDF_JOBOPTIONS = "eaemPDFJobOptions";

app.consoleout("QUERY : " + QUERY);

var response = getResponse(host, QUERY, credentials);

if(!response){
return;
}

var hitMap = JSON.parse(response);

if(!hitMap || (hitMap.hits.length == 0)){
app.consoleout('No Query results....');
return;
}

var aemFilePath = hitMap.hits[0]["path"],
folderPath = getAEMParentFolder(aemFilePath);

app.consoleout("Folder path : " + folderPath);

response = getResponse(host, folderPath + "/jcr:content/metadata.json", credentials);

var metadataMap = JSON.parse(response);

var jobOptionsName = metadataMap[META_EAEM_PDF_JOBOPTIONS];

if(!jobOptionsName){
app.consoleout('jobOptions does not exist in folder metadata....');
return;
}

var exportFolderPdf = new Folder(exportFolder.fullName + "/pdf"),
pdfFileName = fileName + ' - ' + jobOptionsName + '.pdf';

try{
exportFolderPdf.create();

app.consoleout("Creating pdf file " + (exportFolderPdf.fullName + pdfFileName) + " with preset job option - " + jobOptionsName);

var pdfOutputFile = new File(exportFolderPdf.fullName + pdfFileName);

with (app.pdfExportPreferences) {
viewDocumentAfterExport = false;
}

document.exportFile(ExportFormat.pdfType, pdfOutputFile, app.pdfExportPresets.item("[" + jobOptionsName+ "]"));

app.consoleout("Uploading to AEM pdf file " + pdfFileName + ' to location: ' + target + '/jcr:content/renditions');

putResource (host, credentials, pdfOutputFile, pdfFileName, 'application/pdf', target);
}catch (err) {
//app.consoleout(err);
}finally {
cleanup(exportFolderPdf);
}
}

function getAEMNodeName(){
return resourcePath.substring(resourcePath.lastIndexOf("/") + 1);
}

function getAEMParentFolder(filePath){
return filePath.substring(0, filePath.lastIndexOf("/"));
}

function getResponse(host, uri, creds){
var aemConn = new Socket, body = "", response, firstRead = true;

if (!aemConn.open(host, "UTF-8")) {
return;
}

aemConn.write ("GET "+ encodeURI(uri) +" HTTP/1.0");
aemConn.write ("\n");
aemConn.write ("Authorization: Basic " + creds);
aemConn.write ("\n\n");

while( response = aemConn.read() ){
response = response.toString();

if(!firstRead){
body = body + response;
continue;
}

var strings = response.split("\n");

for(var x = 0; x < strings.length; x++){
if( (x == 0) && (strings[x].indexOf("200") === -1)){
return body;
}

if(x === (strings.length - 1)){
body = body + strings[x];
}
}

firstRead = false;
}

return body;
}

6) Add the above script in DAM Update asset workflow's Media Extraction step



7) Upload an InDesign file to the folder in AEM e.g./content/dam/experience-aem; the following messages are logged in InDesign Server console



8) The preset specific PDF is uploaded to renditions folder of the asset by InDesign server, with name <file name> - <preset name>.pdf





AEM 65 - Generate Image or PDF renditions of HTML files uploaded to DAM (AEM Assets)

$
0
0

Goal


Product's DAM Update Asset Workflow (executed on file upload) does not generate renditions of HTML files (whether HTML files uploaded to Assets can serve as web pages from AEM is not discussed here...)

In the following steps we use CommandLineProcess workflow step in DAM Update Asset workflow to generate PNG (or PDF) renditions of HTML files on upload 

Demo | Package Install | Github



Solution


1) Install library wkhtmltopdf from https://wkhtmltopdf.org/ (command line tool to render HTML into PDF and various image formats)


2)  Modify the DAM Update asset workflow - /conf/global/settings/workflow/models/dam/update_asset to add a Command Line step for converting the uploaded HTML file to PNG

                  1. Drag and drop Command Line step

                  2. Specify the allowed mime types - text/html

                  3.  A workaround step to rename the temp file generated by command line process to include .html extension

                                    cmd /c ren ${file} ${filename}.html

                  4.  Execute command to generate the image rendition (you may have to adjust based on output quality, screensize etc.)

                                    C:/dev/install/wkhtmltopdf/bin/wkhtmltoimage.exe --height 1200 ${file}.html thumbnail.png



3) Upload a html file e.g. eaem.html



4) Rendition thumbnail.png generated and added as eaem.html/jcr:content/renditions/thumbnail.png


AEM 65 - Workflow step to generate PDF of Sites page during Activation (Publish)

$
0
0

Goal


Create a PDF version of Sites page in Request for Activation workflow and add it to AEM Assets for archival.

In the following steps, a Process Step for creating PDF of published page with file format  pageName-yyyy-MM-dd-HH-mm-ss.pdf was added to the Request for Activation workflow /conf/global/settings/workflow/models/request_for_activation.html.

Creating PDFs could be useful for handling compliance usecases. Depending on the page design, quality of generated PDF varies and may not be appealing....

For creating thumbnails or renditions of static HTML pages uploaded to AEM check this post

Demo | Package Install | Github


Page in Author



Generated Page PDFs added in Assets



PDF in Reader



Solution


1) Install library wkhtmltopdf from https://wkhtmltopdf.org/ (command line tool to render HTML into PDF and various image formats)


2) Create a workflow Process Step apps.experienceaem.sites.EAEMCreatePDFFromPage with the following code to generate PDFs using above library

                   Method wasPageReplicatedRecently() checks if page was activated in the last 60 minutes to make sure it creates the PDF of latest version

                   Method executeCommand() invokes the tool wkhtmltopdf to generate PDF of page

                   Method createAssetInDAM() adds the generated PDF in Assets

package apps.experienceaem.sites;

import com.adobe.granite.workflow.PayloadMap;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.dam.api.Asset;
import com.day.cq.dam.api.AssetManager;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.replication.Agent;
import com.day.cq.replication.AgentManager;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.exec.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Node;
import javax.jcr.Session;
import java.io.File;
import java.io.FileInputStream;
import java.text.SimpleDateFormat;
import java.util.*;

@Component(
immediate = true,
service = {WorkflowProcess.class},
property = {
"process.label = Experience AEM Create PDF From page"
}
)
public class EAEMCreatePDFFromPage implements WorkflowProcess {
protected final Logger log = LoggerFactory.getLogger(this.getClass());

private static String ARG_COMMAND = "COMMAND";
private static Integer CMD_TIME_OUT = 300000; // 5 minutes
private static SimpleDateFormat PDF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
private static String AGENT_PUBLISH = "publish";
private static String DAM_FOLDER_PREFIX = "/content/dam";
private static int RETRIES = 20;
private static int SLEEP_TIME = 5000; // wait 5 secs

@Reference
private ResourceResolverFactory resolverFactory;

@Reference
private AgentManager agentMgr = null;

@Override
public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap args) throws WorkflowException {
File tmpDir = null;

try {
Session session = wfSession.getSession();
WorkflowData wfData = workItem.getWorkflowData();

String pagePath = null;
String payLoadType = wfData.getPayloadType();

if(payLoadType.equals("JCR_PATH") && wfData.getPayload() != null) {
if(session.itemExists((String)wfData.getPayload())) {
pagePath = (String)wfData.getPayload();
}
} else if( (wfData.getPayload() != null) && payLoadType.equals("JCR_UUID")) {
Node metaDataMap = session.getNodeByUUID((String)wfData.getPayload());
pagePath = metaDataMap.getPath();
}

if(StringUtils.isEmpty(pagePath)){
log.warn("Page path - " + wfData.getPayload() + ", does not exist");
return;
}

ResourceResolver resolver = getResourceResolver(session);
Resource pageResource = resolver.getResource(pagePath);

tmpDir = File.createTempFile("eaem", null);
tmpDir.delete();
tmpDir.mkdir();

File tmpFile = new File(tmpDir, pageResource.getName() + "-" + PDF_DATE_FORMAT.format(new Date()) + ".pdf");
CommandLine commandLine = getCommandLine(pagePath, args, tmpFile);
int count = RETRIES;

do{
Thread.sleep(SLEEP_TIME);

if( !wasPageReplicatedRecently(pageResource) ){
log.debug("Page - " + pageResource.getPath() + ", not replicated, skipping PDF generation");
continue;
}

executeCommand(commandLine);

createAssetInDAM(pagePath, tmpFile, resolver);

session.save();

break;
}while(count-- > 0);
} catch (Exception e) {
log.error("Failed to create PDF of page", e);
}finally{
if(tmpDir != null){
try { FileUtils.deleteDirectory(tmpDir); } catch(Exception ignore){}
}
}
}

private boolean wasPageReplicatedRecently(Resource pageResource){
ValueMap valueMap = pageResource.getChild("jcr:content").getValueMap();
Date lastReplicated = valueMap.get("cq:lastReplicated", Date.class);

if(lastReplicated == null){
return false;
}

Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.HOUR, -1);

Date oneHourBack = cal.getTime();

//check if the page was replicated in last 60 minutes
return lastReplicated.getTime() > oneHourBack.getTime();
}

private void createAssetInDAM(String pagePath, File tmpFile, ResourceResolver resolver)throws Exception{
String rootPath = DAM_FOLDER_PREFIX + pagePath;
String assetPath = rootPath + "/" + tmpFile.getName();

JcrUtil.createPath(DAM_FOLDER_PREFIX + pagePath, "sling:Folder", "sling:Folder",
resolver.adaptTo(Session.class), true);

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

FileInputStream fileIn = new FileInputStream(tmpFile);

Asset asset = assetManager.createAsset(assetPath, fileIn, "application/pdf", false);

log.info("Created asset - " + asset.getPath() + ", for published page - " + pagePath);

IOUtils.closeQuietly(fileIn);
}

private CommandLine getCommandLine(String pagePath, MetaDataMap args, File tmpFile) throws Exception{
String processArgs = args.get("PROCESS_ARGS", String.class);

if(StringUtils.isEmpty(processArgs)){
throw new RuntimeException("No command available in process args");
}

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

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

if(params[0].trim().equals(ARG_COMMAND)){
command = params[1].trim();
break;
}
}

if(StringUtils.isEmpty(command)){
throw new RuntimeException("No command available in process args");
}

HashMap<String, String> parameters = new HashMap<String, String>();
parameters.put("publishPagePath", getPublishPath(pagePath));
parameters.put("timeStampedPDFInAssets", tmpFile.getAbsolutePath());

return CommandLine.parse(command, parameters);
}

private String getPublishPath(String pagePath){
Agent publishAgent = agentMgr.getAgents().get(AGENT_PUBLISH);

String transportURI = publishAgent.getConfiguration().getTransportURI();

String hostName = transportURI.substring(0, transportURI.indexOf("/bin/receive"));

return ( hostName + pagePath + ".html");
}

private void executeCommand(CommandLine commandLine) throws Exception{
DefaultExecutor exec = new DefaultExecutor();
ExecuteWatchdog watchDog = new ExecuteWatchdog(CMD_TIME_OUT);

exec.setWatchdog(watchDog);
exec.setStreamHandler(new PumpStreamHandler(System.out, System.err));
exec.setProcessDestroyer(new ShutdownHookProcessDestroyer());

int exitValue = exec.execute(commandLine);

if(exec.isFailure(exitValue)){
throw new RuntimeException("Error creating PDF, command returned - " + exitValue);
}
}

private ResourceResolver getResourceResolver(final Session session) throws LoginException {
return resolverFactory.getResourceResolver( Collections.<String, Object>
singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session));
}
}

               
3) Add a Process Step with the following argument at the end of Request for Activation workflow to execute PDF creation process

                              COMMAND=C:/dev/install/wkhtmltopdf/bin/wkhtmltopdf ${publishPagePath} ${timeStampedPDFInAssets}







AEM 65 - Touch UI Show Full Title in Sites and Assets Tags Picker (and not Ellipsis)

$
0
0

Goal


In Touch UI, show the full title of tag in picker....

For AEM 63 check this post

Demo | Package Install | Github


Product



Extension - Assets



Extension - Sites




Solution


1) Login to CRXDE Lite (http://localhost:4502/crx/de), create folder /apps/eaem-touchui-tags-picker-show-full-title

2) Create node /apps/eaem-touchui-tags-picker-show-full-title/clientlib of type cq:ClientLibraryFolder, add String property categories with value cq.ui.coral.common.tagfield, String[] property dependencies with value lodash

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

                        show-full-title.js

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

(function($, $document) {
var TAGS_FIELD = "cq:tags",
LETTER_COUNT = 22, INCREASE_BY = 1.5, CV_ITEM_HEIGHT = 3, CV_LABEL_HEIGHT = 2,
extended = false;

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

function handleTagsPicker(){
if(extended){
return;
}

var $tagsField = $("foundation-autocomplete[name$='" + TAGS_FIELD + "']");

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

var pathField = $tagsField[0];

extended = true;

extendPicker(pathField);
}

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

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

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

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

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

showFullTitle(dummyEvent);
}
}

function showFullTitle(event){
var $item, $content, increase, $cvItem;

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

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

increase = (INCREASE_BY * Math.floor($content.html().length / LETTER_COUNT));

if( ($item.prop("variant") == "drilldown") && (increase > 0)){
increase++;
}

$item.css("height", (CV_ITEM_HEIGHT + increase) + "rem");

$content.css("height",(CV_LABEL_HEIGHT + increase) + "rem").css("white-space", "normal");
});
}
})(jQuery, jQuery(document));


Viewing all 525 articles
Browse latest View live