Resource Listener to flush cache in AEM 6.5

Use a Resource Listener and flush cached paths in AEM 6.5

Goal

Create a resource listener (OSGi component) in AEM 6.5 listening on added/changed resource.

Use Case

Flush specific cached paths from the publishers based on specific conditions. These paths are retrieved with a Query.

Procedure

To flush the cache I’ll use the dispacherFlusher service, available in the ACS COMMONS. For this reason, you need install the ACS COMMONS package on your instance and import in your POM.xml the following dependency (check your version!):

<dependency>
	<groupId>com.adobe.acs</groupId>
	<artifactId>acs-aem-commons-bundle</artifactId>
	<version>4.3.0</version>
	<scope>provided</scope>
</dependency>

Now we can create our resource listener, but before that, let me explain what was the requirement.

We had a Carousel component, used in different pages, that could be configured as:

  • static: the authors configured the slides to show
  • dynamic: the slides displayed were the last modified articles with at least one tag in common with the current page.

So, in case an author modified and published a page, we had to check for all the other pages with a dynamic component. If they had common tags, those pages had to show that more recent article and the cache needed to be flushed. Anyway, this is just an example and you can customize it as you need.

In order to create a ResourceListener is necessary to implement the ResourceChangeListener interface and configure the “path” and “changes” properties
Just a few remarks:

  • Possible values of “CHANGES” are here.
  • It’s been used a resource resolver associated with a system user with specific ACLs. If you don’t know how to create it, check this article.

Hoping it’s clear 😁, here is the code:

package com.adobe.training.core.listeners;

import com.adobe.acs.commons.replication.dispatcher.DispatcherFlusher;
import com.day.cq.replication.Agent;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationResult;
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 com.day.cq.tagging.Tag;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
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.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.settings.SlingSettingsService;
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.RepositoryException;
import javax.jcr.Session;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * The type Page activation listener.
 */
@Component(
        service = ResourceChangeListener.class,
        property = {
                ResourceChangeListener.PATHS + "=" + "/content/Lab2020",
                ResourceChangeListener.CHANGES + "=" + "ADDED",
                ResourceChangeListener.CHANGES + "=" + "CHANGED"
        }
)
public class PageActivationListener implements ResourceChangeListener {

    /**
     * Constant for Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(PageActivationListener.class);

    /**
     * The constant PUBLISH.
     */
    private static final String PUBLISH = "publish";

    /**
     * AUTH_INFO.
     */
    private static final Map<String, Object> AUTH_INFO = Collections.singletonMap("sling.service.subservice", "acs-commons-dispatcher-flush-service");

    /**
     * dispatcherFlusher.
     */
    @Reference
    private DispatcherFlusher dispatcherFlusher;

    /**
     * resourceResolverFactory.
     */
    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    /**
     * Query Builder.
     */
    @Reference
    private QueryBuilder queryBuilder;

    /**
     * private reference for slingSettingsService.
     */
    @Reference
    private SlingSettingsService slingSettingsService;

    /**
     * on change method.
     * The method handles the flushing of pages with tags in common and dynamic carousel
     */
    @Override
    public void onChange(List<ResourceChange> list) {
        if (slingSettingsService.getRunModes().contains(PUBLISH)) {
            //changes are all related to the nodes of the same page. I just need to get one path and obtain the page path
            ResourceChange resourceChange = list.get(0);
            String resourcePath = resourceChange.getPath();
            Page currentPage = null;
            try (ResourceResolver flushingResourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH_INFO)) {
                PageManager pageManager = flushingResourceResolver.adaptTo(PageManager.class);
                if (pageManager != null) {
                    currentPage = pageManager.getContainingPage(resourcePath);
                }
                if (currentPage != null) {
                    Page homepage = currentPage.getAbsoluteParent(4);
                    List<String> pathsWithSimilarTags = findPagePathWithSimilarTags(currentPage, homepage, flushingResourceResolver);
                    for (String pathToFlush : pathsWithSimilarTags) {
                        LOG.info("Page path: " + pathToFlush);
                        final Map<Agent, ReplicationResult> results = dispatcherFlusher.flush(flushingResourceResolver,
                                ReplicationActionType.ACTIVATE, true, pathToFlush);
                        for (final Map.Entry<Agent, ReplicationResult> entry : results.entrySet()) {
                            final Agent agent = entry.getKey();
                            final ReplicationResult result = entry.getValue();
                            LOG.info("Agent: " + agent.getId());
                            LOG.info("Page '" + result.getMessage() + "'" + result.isSuccess());
                        }
                    }
                }
            } catch (ReplicationException e) {
                LOG.error("Replication exception occurred during Dispatcher Flush request.", e);
            } catch (LoginException e) {
                LOG.error("Failed to get Resource resolver", e);
            }
        }
    }


    /**
     * Find page paths with similar tags list and with carouselType dynamic.
     *
     * @param currentPage      the current page
     * @param homepage         the home page
     * @param resourceResolver the resource resolver
     * @return the list
     */
    private List<String> findPagePathWithSimilarTags(Page currentPage, Page homepage, ResourceResolver resourceResolver) {
        final List<String> pageList = new ArrayList<>();
        if (homepage != null) {
            //Tag[] currentPageTags = new Tag[0];// TagUtils.getPageTagsFilterByNamespace(currentPage);
            Tag[] currentPageTags = currentPage.getTags();
            Session session = resourceResolver.adaptTo(Session.class);
            Map<String, String> queryParameters = new HashMap<>();
            queryParameters.put("path", homepage.getPath());
            queryParameters.put("type", "cq:page");
            queryParameters.put("property", "carouselType");
            queryParameters.put("property.value", "dynamic");
            queryParameters.put("property.depth", "6");
            if (currentPageTags != null && currentPageTags.length > 0) {
                queryParameters.put("group.p.or", "true");
                for (int i = 0; i < currentPageTags.length; i++) {
                    queryParameters.put("group." + i + "_tagid", currentPageTags[i].getTagID());
                    queryParameters.put("group." + i + "_tagid.property", "jcr:content" + "/" + "cq:tags");
                }
            }
            Query query = queryBuilder.createQuery(PredicateGroup.create(queryParameters), session);
            SearchResult result = query.getResult();

            // iterating over the results
            for (Hit hit : result.getHits()) {
                try {
                    String path = hit.getPath();
                    if (!StringUtils.equals(path, currentPage.getPath())) { // don't add in list if result is currentPage
                        pageList.add(path);
                    }
                } catch (RepositoryException e) {
                    LOG.error("RepositoryException while retrieving results", e);
                }
            }
        }
        return pageList;
    }
}

That’s all! This has been tested on AEM 6.5 but I’m pretty sure that can work on other AEM versions.

If you need to create an event listener, please have a look at this article. Here instead, you can find different articles with some OSGi examples.

Cheers! 🍛

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: