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! 🍛