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