AEM Cloud Service - Content Fragments Usage in Pages Custom Asset Report

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