AEM Cloud Service - Dynamic Media Clear Scene7 CDN Cache on Asset ReUpload

Goal

Default Scene7 CDN TTL for an asset url is 10 hours. Deleting an asset uploaded to AEM (synced to Scene7) and uploading another binary with same name, will not serve the respective presets with latest binary on delivery until the previous ones expire in CDN or the cache invalidated using Tools > Assets CDN Invalidation. This post is on automatically invalidating urls in CDN cache when the asset is reuploaded...

Demo | Package Install | Github


Solution

1) Add a Resource change listener apps.experienceaem.assets.core.listeners.S7CDNCacheInvalidateListener for invalidating the CDN cache using a configured template...

package apps.experienceaem.assets.core.listeners;

import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.engine.SlingRequestProcessor;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.*;

@Component(
        service = ResourceChangeListener.class,
        immediate = true,
        property = {
                ResourceChangeListener.CHANGES + "=CHANGED",
                ResourceChangeListener.PATHS + "=glob:/content/dam"
        })
@ServiceDescription("EAEM - Invalidate CDN Cache on Asset Create")
public class S7CDNCacheInvalidateListener implements ResourceChangeListener {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private static final String S7_CDN_INVALIDATE_SERVLET = "/mnt/overlay/dam/gui/content/s7dam/cdninvalidation/cdninvalidationwizard.s7cdninvalidation.json";

    private static final String S7_CDN_INVALIDATE_URLS = "/mnt/overlay/dam/gui/content/s7dam/cdninvalidation/cdninvalidationwizard.s7cdncacheurls.json";

    @Reference
    ResourceResolverFactory resolverFactory;

    @Reference
    private SlingRequestProcessor slingRequestProcessor;

    @Reference
    private RequestResponseFactory requestResponseFactory;

    public void onChange(List<ResourceChange> changes) {
        String assetMetadataPath = null;
        Iterator<ResourceChange> changesItr = changes.iterator();
        ResourceChange change = null;

        while(changesItr.hasNext()){
            change = changesItr.next();

            if(!change.getPath().endsWith("/jcr:content/metadata")){
                continue;
            }

            assetMetadataPath = change.getPath();

            break;
        };

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

        Map<String, Object> subServiceUser = new HashMap<String, Object>();
        subServiceUser.put(ResourceResolverFactory.SUBSERVICE, "eaem-user-s7-admin");

        ResourceResolver resourceResolver = null;

        try {
            resourceResolver = resolverFactory.getServiceResourceResolver(subServiceUser);

            Resource assetMetaRes = resourceResolver.getResource(assetMetadataPath);

            String scene7FileName = assetMetaRes.getValueMap().get("dam:scene7File", "");

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

            List<String> urls = getInvalidateUrls(resourceResolver, assetMetadataPath);

            if(CollectionUtils.isEmpty(urls)){
                logger.info("Nothing to s7 cdn invalidate for asset " + assetMetadataPath);
                return;
            }

            logger.info("Invalidate the urls : " + urls);

            invalidateS7CDNCache(resourceResolver, urls);
        } catch (final Exception e) {
            logger.error("Error invalidating cache : {}", e.getMessage(), e);
        } finally {
            if ((resourceResolver != null) && resourceResolver.isLive()) {
                resourceResolver.close();
            }
        }
    }

    private void invalidateS7CDNCache(ResourceResolver resourceResolver, List<String> urls) throws Exception{
        Map<String, Object> requestParams = new LinkedHashMap<String, Object>();
        requestParams.put("urls", String.join("&", urls));

        HttpServletRequest request = requestResponseFactory.createRequest("POST", S7_CDN_INVALIDATE_SERVLET,
                                        requestParams);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        HttpServletResponse response = this.requestResponseFactory.createResponse(bos);
        HttpServletRequestWrapper wrapperRequest = new URLsSlingServletRequestWrapper(request);

        slingRequestProcessor.processRequest(wrapperRequest, response, resourceResolver);

        response.getWriter().flush();

        JSONObject invalidateResponse = new JSONObject(bos.toString());

        if(!invalidateResponse.has("invalidationHandle")){
            return;
        }

        logger.info("Successfully invalidate s7 cache for urls " + urls);
    }

    private List<String> getInvalidateUrls(ResourceResolver resourceResolver, String assetMetadataPath) throws Exception{
        Map<String, Object> requestParams = new HashMap<String, Object>();
        requestParams.put("paths", assetMetadataPath.substring(0, assetMetadataPath.indexOf("/jcr:content")));
        requestParams.put("template", "true");
        requestParams.put("presets", "true");

        HttpServletRequest request = requestResponseFactory.createRequest("POST", S7_CDN_INVALIDATE_URLS,
                requestParams);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        HttpServletResponse response = this.requestResponseFactory.createResponse(bos);

        slingRequestProcessor.processRequest(request, response, resourceResolver);

        response.getWriter().flush();

        JSONObject invalidateURLsJSON = new JSONObject(bos.toString());

        List<String> invalidateUrls = new ArrayList<String>();

        if(!invalidateURLsJSON.has("urls")){
            return invalidateUrls;
        }

        JSONArray invalidateArray = invalidateURLsJSON.getJSONArray("urls");

        for(int i = 0; i < invalidateArray.length(); i++){
            invalidateUrls.add(invalidateArray.getString(i));
        }

        return invalidateUrls;
    }

    private class URLsSlingServletRequestWrapper extends HttpServletRequestWrapper {
        public URLsSlingServletRequestWrapper(final HttpServletRequest request) {
            super(request);
        }

        public Map<String, String[]> getParameterMap() {
            Map<String, String[]> pMap = super.getParameterMap();

            String[] urls = String.valueOf(pMap.get("urls")[0]).split("&");

            pMap.put("urls", urls);

            return pMap;
        }
    }
}


2) Access Tools > Assets > CDN Invalidation Template to add the invalidate URL formats. <ID> placeholder in the urls is replaced with the asset s7 name (dam:scene7File) when the asset is uploaded and metadata added....

                      https://author-p10961-e90064.adobeaemcloud.com/mnt/overlay/dam/gui/content/s7dam/cdninvalidationtemplate/cdninvalidationtemplate.html


3) Create a sling repo init script ui.config\src\main\content\jcr_root\apps\eaem-asset-s7-cdn-invalidate\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config for adding a service user eaem-user-s7-admin used in report generation logic

scripts=[
        "
        create service user eaem-user-s7-admin with path system/cq:services/experience-aem
        set principal ACL for eaem-user-s7-admin
                allow jcr:read on /content/dam
        end
        "
]


4) Add the necessary bundle to service user mapping script ui.config\src\main\content\jcr_root\apps\eaem-asset-s7-cdn-invalidate\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-asset-s7-cdn-invalidate.core:eaem-user-s7-admin=[eaem-user-s7-admin]]"/>


5) When a user uploads asset, the following statements are logged in author_aemerror_xxxxxxx.log

                                     30.07.2021 22:01:51.422 [cm-p10961-e90064-aem-author-bcbb9c567-f52x8] *INFO* [sling-oak-observation-20] apps.experienceaem.assets.core.listeners.S7CDNCacheInvalidateListener Successfully invalidate s7 cache for urls [https://s7d1.scene7.com/is/image/EAEM/right$S7%20Product$, https://s7d1.scene7.com/is/image/EAEM/right?$EAEM$]


1 comment:

  1. hi, does this new implemented event also trigger after a cropping, changed by the author on the asset image itself ?

    ReplyDelete