Goal
Adobe Experience Manager 2021.7.5607.20210705T063041Z-210600
Create a Custom Asset Report Experience AEM Content Fragments Report for finding the usage of Content Fragments in a Site (or a section of site with root path)
Demo | Package Install | Github
Select Report Type
Report Configuration
Select Columns in Report
Reports Generated
Report View
Solution
1) Create the report type node /apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
description="Experience AEM Custom Reports Content Fragments Usage in Site Pages"
icon="data"
type="cf-usage-report"
title="Experience AEM Content Fragments Report"
wizard="/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report-wizard.html"/>
2) Create report wizard /apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report-wizard
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column1
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<field1
jcr:primaryType="nt:unstructured"
jcr:title="Title and Description"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<name
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="jobTitle"
required="{Boolean}true"/>
<description
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
fieldLabel="Description"
name="jobDescription"
rows="4"/>
</items>
</field1>
<field2
jcr:primaryType="nt:unstructured"
jcr:title="Content Fragments Root Path"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<pathbrowser
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
emptyText="/content/dam"
fieldLabel="Folder Path"
filter="folder"
name="cfRootPath"
predicate="folder"
rootPath="/content/dam"/>
</items>
</field2>
<field3
jcr:primaryType="nt:unstructured"
jcr:title="Pages Root Path"
sling:resourceType="granite/ui/components/coral/foundation/form/fieldset">
<items jcr:primaryType="nt:unstructured">
<pathbrowser
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
emptyText="/content"
fieldLabel="Site Path"
name="pageRootPath"
predicate="folder"
rootPath="/content"/>
</items>
</field3>
</items>
</column1>
</items>
</jcr:root>
3) Add a filter apps.experienceaem.assets.core.filters.AddCustomReports to add the custom CF report cf-usage-report to otb reports data source dam/gui/coral/components/commons/ui/shell/datasources/reportlistdatasource pulling from /libs/dam/content/reports/availablereports
package apps.experienceaem.assets.core.filters;
import com.adobe.granite.ui.components.Config;
import com.adobe.granite.ui.components.ExpressionResolver;
import com.adobe.granite.ui.components.PagingIterator;
import com.adobe.granite.ui.components.ds.AbstractDataSource;
import com.adobe.granite.ui.components.ds.DataSource;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.iterators.TransformIterator;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceWrapper;
import org.apache.sling.api.resource.ValueMap;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Component(
service = Filter.class,
immediate = true,
name = "Experience AEM Custom Reports - Add Custom Reports Filter",
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
"sling.filter.scope=INCLUDE",
"sling.filter.resourceTypes=dam/gui/coral/components/commons/ui/shell/datasources/reportlistdatasource"
}
)
public class AddCustomReports implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static String ASSET_USAGE_REPORT = "/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report";
private static String OTB_REPORTS_PATH = "dam/content/reports/availablereports";
@Reference
private ExpressionResolver expressionResolver;
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain filterChain) throws IOException, ServletException {
final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
Resource resource = slingRequest.getResource();
Resource repConfigRes = resource.getChild(Config.DATASOURCE);
filterChain.doFilter(request, response);
if(repConfigRes == null){
return;
}
ValueMap repConfigVM = repConfigRes.getValueMap();
String reportPath = repConfigVM.get("reportPath", "");
String itemRT = repConfigVM.get("itemResourceType", "");
if(!OTB_REPORTS_PATH.equals(reportPath) || StringUtils.isEmpty(itemRT)){
return;
}
AbstractDataSource ds = (AbstractDataSource)request.getAttribute(DataSource.class.getName());
final List<Resource> sortedList = new ArrayList<Resource>();
Iterator<Resource> items = ds.iterator();
while(items.hasNext()){
sortedList.add(items.next());
}
sortedList.add(slingRequest.getResourceResolver().getResource(ASSET_USAGE_REPORT));
ds = new AbstractDataSource() {
public Iterator<Resource> iterator() {
return new TransformIterator(new PagingIterator(sortedList.iterator(), 0, 100), new Transformer() {
public Object transform(Object o) {
final Resource r = (Resource)o;
return new ResourceWrapper(r) {
public String getResourceType() {
return itemRT;
}
};
}
});
}
};
request.setAttribute(DataSource.class.getName(), ds);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
4) Create a servlet apps.experienceaem.assets.core.servlets.EAEMReportGeneration for handling the requests from report creation wizard /mnt/overlay/dam/gui/content/reports/createreportwizard.html and create a sling job, put in the queue com/eaem/aem/dam/report for report generation...
package apps.experienceaem.assets.core.servlets;
import com.day.cq.commons.TidyJSONWriter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.JobManager;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;
@Component(
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.selectors=eaemcfreport",
"sling.servlet.methods=POST",
"sling.servlet.resourceTypes=sling/servlet/default"
}
)
public class EAEMReportGeneration extends SlingAllMethodsServlet {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Reference
private JobManager jobManager;
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
ResourceResolver resourceResolver = request.getResourceResolver();
try{
String jobNodeName = UUID.randomUUID().toString();
HashMap<String, Object> jobProps = new HashMap<String, Object>();
jobProps.put("cfRootPath", request.getParameter("cfRootPath"));
jobProps.put("pageRootPath", request.getParameter("pageRootPath"));
jobProps.put("jobNodePath", "/var/dam/reports/" + jobNodeName);
List<String> columns = new ArrayList<String>();
columns.addAll(Arrays.asList(request.getParameterValues("column")));
columns.add("Page References");
jobProps.put("reportColumns", columns.toArray(new String[0]));
Node jobNode = createJobNode(resourceResolver, jobNodeName, request, columns);
Calendar createTime = Calendar.getInstance();
createTime.setTimeInMillis(createTime.getTimeInMillis());
Job job = jobManager.addJob("com/eaem/aem/dam/report", jobProps);
jobNode.setProperty("jobId", job.getId());
jobNode.setProperty("jobStatus", "processing");
jobNode.setProperty("jcr:created", createTime);
TidyJSONWriter writer = new TidyJSONWriter(response.getWriter());
writer.object();
writer.key("jobNodeName").value(jobNodeName);
writer.endObject();
resourceResolver.commit();
}catch(Exception e){
logger.error("Error scheduling export job", e);
}
}
private Node createJobNode(ResourceResolver resourceResolver, String jobNodeName,
SlingHttpServletRequest request, List<String> columns)
throws RepositoryException {
Session session = resourceResolver.adaptTo(Session.class);
String baseNodePath = "/var/dam/reports";
Node baseNode, jobNode;
if(resourceResolver.getResource(baseNodePath) == null) {
jobNode = session.getNode("/var/dam");
baseNode = jobNode.addNode("reports", "sling:Folder");
} else {
baseNode = session.getNode(baseNodePath);
}
jobNode = baseNode.addNode(jobNodeName.replaceAll("/", "-"), "nt:unstructured");
jobNode.setProperty("reportType", request.getParameter("dam-asset-report-type"));
jobNode.setProperty("cfRootPath", request.getParameter("cfRootPath"));
jobNode.setProperty("pageRootPath", request.getParameter("pageRootPath"));
jobNode.setProperty("jobTitle", request.getParameter("jobTitle"));
jobNode.setProperty("jobDescription", request.getParameter("jobDescription"));
jobNode.setProperty("reportColumns", columns.toArray(new String[0]));
session.save();
return jobNode;
}
}
5) Create a job consumer apps.experienceaem.assets.core.reports.EAEMReportJobConsumer, add the necessary search logic for finding the content fragments added on pages and create report (stored in /var/dam/reports)
package apps.experienceaem.assets.core.reports;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import java.io.*;
import java.util.*;
@Component(
immediate = true,
service = JobConsumer.class,
property = {
"job.topics=com/eaem/aem/dam/report",
}
)
public class EAEMReportJobConsumer implements JobConsumer {
protected final Logger logger = LoggerFactory.getLogger(EAEMReportJobConsumer.class);
private static String CF_USAGE_REPORT_TYPE = "cf-usage-report";
@Reference
private ResourceResolverFactory resolverFactory;
@Reference
private QueryBuilder builder;
public JobConsumer.JobResult process(Job job) {
ResourceResolver resourceResolver = null;
File tempFile = null;
try {
resourceResolver = getServiceResourceResolver(resolverFactory);
Session session = resourceResolver.adaptTo(Session.class);
String cfRootPath = job.getProperty("cfRootPath", String.class);
if(StringUtils.isEmpty(cfRootPath)){
cfRootPath = "/content/dam";
}
String pageRootPath = job.getProperty("pageRootPath", String.class);
if(StringUtils.isEmpty(pageRootPath)){
pageRootPath = "/content";
}
String csvCreationPath = job.getProperty("jobNodePath", String.class);
Node jobNode = session.getNode(csvCreationPath);
String csvName = jobNode.getProperty("jobTitle").getString();
ArrayList columns = new ArrayList(Arrays.asList(job.getProperty("reportColumns", String[].class)));
String reportType = jobNode.getProperty("reportType").getString();
if(!reportType.equals(CF_USAGE_REPORT_TYPE)){
return JobResult.OK;
}
tempFile = File.createTempFile("report", ".csv");
FileOutputStream e = new FileOutputStream(tempFile);
PrintWriter writer = new PrintWriter(new OutputStreamWriter(e, "UTF-8"));
List csvHeaders = writeColumnsHeaderToCSV(writer, columns);
writeContentToCSV(writer, resourceResolver, cfRootPath,pageRootPath);
writer.close();
Node fileNode = jobNode.addNode(csvName + ".csv", "nt:file");
Node resNode = fileNode.addNode("jcr:content", "nt:resource");
jobNode.setProperty("reportCsvColumns", (String[])csvHeaders.toArray(new String[0]));
resNode.setProperty("jcr:mimeType", "text/csv");
resNode.setProperty("jcr:data", session.getValueFactory().createBinary(
new ByteArrayInputStream(FileUtils.readFileToByteArray(tempFile))));
setLastModified(resNode);
jobNode.setProperty("jobStatus", "completed");
session.save();
} catch (Exception var28) {
logger.info("Failed to create report");
return JobResult.FAILED;
} finally {
if(tempFile != null) {
tempFile.delete();
}
}
return JobResult.OK;
}
public ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
Map<String, Object> subServiceUser = new HashMap<>();
subServiceUser.put(ResourceResolverFactory.SUBSERVICE, "eaem-user-report-admin");
try {
return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
} catch (LoginException ex) {
logger.error("Could not login as SubService user {}", "eaem-user-report-admin", ex);
return null;
}
}
public List<String> writeColumnsHeaderToCSV(PrintWriter writer, List<String> columns) throws IOException {
List<String> csvColumns = new ArrayList<String>();
columns.stream().forEach((c) -> {
writer.append("\"").append(c.toUpperCase()).append("\"").append(",");
csvColumns.add(c.toLowerCase());
});
writer.append("\r\n");
return csvColumns;
}
private static Map<String, String> getFindCFsQueryPredicateMap(String folderPath) {
Map<String, String> map = new HashMap<>();
map.put("path", folderPath);
map.put("1_property","jcr:content/contentFragment");
map.put("1_property.value","true");
map.put("p.limit","-1");
return map;
}
private static Map<String, String> getFindReferencesPredicateMap(String folderPath, String cfPath) {
Map<String, String> map = new HashMap<>();
map.put("path", folderPath);
map.put("fulltext", cfPath);
map.put("orderby", "@jcr:score");
map.put("p.limit", "-1");
return map;
}
private void setLastModified(Node resNode) throws RepositoryException {
Calendar lastModified = Calendar.getInstance();
lastModified.setTimeInMillis(lastModified.getTimeInMillis());
resNode.setProperty("jcr:lastModified", lastModified);
}
public void writeContentToCSV(PrintWriter writer, ResourceResolver resolver, String folderPath, String pageRootPath) throws Exception {
Query query = builder.createQuery(PredicateGroup.create(getFindCFsQueryPredicateMap(folderPath)), resolver.adaptTo(Session.class));
SearchResult result = query.getResult();
String cfPath = null, title;
for (Hit hit : result.getHits()) {
cfPath = hit.getPath();
Query cfQuery = builder.createQuery(PredicateGroup.create(getFindReferencesPredicateMap(pageRootPath, cfPath)), resolver.adaptTo(Session.class));
SearchResult cfResults = cfQuery.getResult();
for (Hit cfHit : cfResults.getHits()) {
writer.append("\"").append(hit.getTitle()).append("\"").append(",").append("\"")
.append(hit.getPath()).append("\"").append(",").append("\"").append(cfHit.getPath()).append("\",");
writer.append("\r\n");
}
}
}
}
6) Create a clientlib /apps/eaem-cs-asset-ref-report/clientlibs/report-list with categories=cq.dam.admin.reportlist and dependencies=eaem.lodash for adding the report type in list view...
(function ($, $document) {
"use strict";
var _ = window._,
initialized = false,
REPORT_TYPE_DETAIL = "Experience AEM Content Fragments Usage Report",
REPORT_LIST_PAGE = "/mnt/overlay/dam/gui/content/reports/reportlist.html";
if (!isReportListPage()) {
return;
}
init();
function init(){
if(initialized){
return;
}
initialized = true;
$(document).one("foundation-contentloaded", function(e){
$("[value='null']").html(REPORT_TYPE_DETAIL);
});
}
function isReportListPage() {
return (window.location.pathname.indexOf(REPORT_LIST_PAGE) >= 0);
}
}(jQuery, jQuery(document)));
7) Create a clientlib /apps/eaem-cs-asset-ref-report/clientlibs/report-wizard with categories=cq.dam.admin.createreportwizard and dependencies=eaem.lodash to change the form action when report type is cf-usage-report
(function ($, $document) {
"use strict";
var _ = window._,
initialized = false,
REPORT_TYPE = "cf-usage-report",
EXPORT_REQ_URL = "/apps/eaem-cs-asset-ref-report/asset-reports/cf-usage-report.eaemcfreport.json",
REPORT_TYPE_DETAIL = "Experience AEM Content Fragments Usage Report",
REPORT_WIZARD = "/mnt/overlay/dam/gui/content/reports/createreportwizard.html";
if (!isReportWizard()) {
return;
}
init();
function init(){
if(initialized){
return;
}
initialized = true;
$(document).one("foundation-contentloaded.cq-damadmin-createreport-wizard", function(e){
$(".foundation-wizard", e.target).on("foundation-wizard-stepchange", setExportUrl);
});
}
function setExportUrl(e, to){
if( (getStepNumber() != 3) || (REPORT_TYPE !== getReportType())){
return;
}
var $lastStep = $(to);
$lastStep.on("foundation-contentloaded",function(){
$(".cq-dam-assetthumbnail").find("coral-card-title").html(REPORT_TYPE_DETAIL);
$("form").prop("action", EXPORT_REQ_URL);
});
}
function getStepNumber(){
var $wizard = $(".foundation-wizard"),
currentStep = $wizard.find(".foundation-wizard-step-active"),
wizardApi = $wizard.adaptTo("foundation-wizard");
return wizardApi.getPrevSteps(currentStep).length + 1;
}
function getReportType(){
var reportPath = $("input[name='dam-asset-report-type'] ").val();
return reportPath.substring(reportPath.lastIndexOf("/") + 1);
}
function isReportWizard() {
return (window.location.pathname.indexOf(REPORT_WIZARD) >= 0);
}
}(jQuery, jQuery(document)));
7) Create a sling repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-asset-ref-report\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config for adding a service user eaem-user-report-admin used in report generation logic
scripts=[
"
create service user eaem-user-report-admin with path system/cq:services/experience-aem
set principal ACL for eaem-user-report-admin
allow jcr:all on /var
allow jcr:read on /apps
allow jcr:read on /content
end
"
]
8) Add the necessary bundle to service user mapping script ui.config\src\main\content\jcr_root\apps\eaem-cs-asset-ref-report\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-eaem.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:OsgiConfig"
user.mapping="[eaem-cs-asset-ref-report.core:eaem-user-report-admin=[eaem-user-report-admin]]"/>





No comments:
Post a Comment