In part 1 of this series, I discussed a method for getting AEM Asset Collection items into the JSON representation of a page. This uses the Sling model + HTL component pattern to obtain the collection items and display their values in the JSON, based on asset type. Specifically, the Sling model enables the JSON renderer to display markup in content fragments and SVGs. It also enables the AEM path to binary formatted assets.
With this method, third party apps and services can consume the JSON, using AEM in a headless manner. Using this approach, authors can update the collections, while the content consumers see those collection updates in their JSON feed. This is a powerful solution already, but we can add some automation in updating the JSON and making it available on the customer/consumer-facing dispatcher domain.
Automating the JSON Updates
Let’s start with automating the JSON updates. When an author adds or removes items in one of the collections, this should be automatically reflected in the JSON. To do this, we need to add a JCR Event Listener class to our project which will listen for changes and request the page with our components. You can find such a class here:
Note: The reference to a CollectionsListenerConfig class. Instead of hard coding the path to the page node, we can obtain an array of paths from the configuration to call one or more pages configured for JSON consumption.
pages = config.pagesToUpdate();
In order to listen for changes to the collections, we create a session using a Service user. This is a special type of AEM user principal, better suited for backend service tasks than a real user ID.
The created session object can then be used to set up the listener. We need to listen for property changes as well as actual node additions and removals under /content/dam/collections.
session.getWorkspace().getObservationManager().addEventListener(this, Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED | Event.NODE_ADDED | Event.NODE_REMOVED, "/content/dam/collections", true,null, null, false);
When one of these events occurs, the onEvent method is invoked. This method is where we make a request for each page in the array obtained from the CollectionsListenerConfig. This uses a simple HTTP servlet request for each page, using the same resource resolver obtained via the service user, in the activate method.
We are not concerned with rendering the pages in a browser, only to call for the pages. This will cause Sling and the HTL engine to process our collections component(s) configured on these pages. Thereby, you trigger the update to their properties with the latest DAM collections data.
Now about that CollectionsListenerConfig class. This is a simple class that uses the ObjectClassDefinition annotation to define the configuration object and an AttributeDefinition annotation to define the configurable field.
@AttributeDefinition(name = "Pages to Update", description = "The pages that should be invoked via Sling when DAM collections are updated or changed.", type = AttributeType.STRING ) String[] pagesToUpdate();
When this class is loaded with the bundle, its configuration will be added to the OSGi config admin service’s list of configurable items (provided via the Felix OSGi implementation). AEM administrators will be able to configure the list of pages for this under /system/console/configMgr. This configuration could also be included in your code repository, under a run mode-specific config folder.
With these additions, the solution now provides automatic updating of the JSON in a configurable manner.
Allowing for Automatic Replication
One more optional thing we can do is update the IncludeCollectionsModel class to allow for automatic replication. An important thing to do here will be to ensure replication is only being done on an author. We can use the replicator API and the externalizer for this.
Two methods have been added to the class, checkIfAuthor and replicateContent. The first method is self-explanatory, it’s a boolean method to determine if this is being executed on an Author or not.
private boolean checkIfAuthor() { Set<String> modes = slingSettingsService.getRunModes(); if (modes.contains(Externalizer.AUTHOR)) { return true; } else { return false; } }
The second method calls the replicator API to replicate the page.
public void replicateContent(String path) { try { Session session = resourceResolver.adaptTo(Session.class); replicator.replicate(session, ReplicationActionType.ACTIVATE, path); log.info("{} Collections have been replicated.", LOG_PREFIX); } catch (ReplicationException e) { log.error(e.getMessage(), e); } }
The call to replicate needs a session, replication action, and resource path. We obtain a session object by adapting the resource resolver provided by Sling (this is the same resource resolver used in the component to obtain the sling:members for the provided collection path). The benefit of using this session is that it was created in the context of the current user’s applied ACLs. This means that replication will occur only if the service user or an author editing the component has to replicate permissions set to allow for the page.
We call this method just after the changes to the component properties have been committed, so the replicated page always has the latest updates. Before calling this, we check to ensure the class is being executed on an author and only replicate if checkIfAuthor returns true.
resourceResolver.commit(); //Replicate the page if this is run on an Author, so the same JSON updates are accessible from the publisher(s). PageManager pageManager= resource.getResourceResolver().adaptTo(PageManager.class); Page currentPage = pageManager.getContainingPage(resource); if (checkIfAuthor() && currentPage != null) { replicateContent(currentPage.getPath()); }
With all of these changes, we’ve maximized automated updates on both authors and publishers. Everything done here uses AEM built-in Sling, Node and Replicator APIs, as well as built-in OSGi services. This illustrates the benefits of extending AEM’s already good building blocks!