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

Goal

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

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

Demo | Package Install | Github


External Usage in Metadata Editor


Curl command to post Usage


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


Stored in CRX


Metadata Schema field


Solution

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

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

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


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

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


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

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

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

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

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

    if(eaemResource == null){
        return;
    }

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

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

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


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

package apps.experienceaem.assets.core.listeners;

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

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

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

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

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

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

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

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

                if(res == null){
                    continue;
                }

                resVM = res.getValueMap();

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

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

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


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

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


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

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

No comments:

Post a Comment