Author: Sreenivas B
Figure 1: Sample Use Case (Listening and Handling Copy/Paste Event)
This article will help you to do any post processing activity on copy/paste of an asset/page — here, I explain one of the important use cases which I have experienced, for which I needed this.
When we integrate AEM with any cloud solution (Adobe or third party) for synchronizing assets or pages, we use some kind of reference ID for linking the content between AEM and the cloud service. For example, in the case of exporting an Experience Fragment to Adobe Target, we use Offer ID for linking the AEM XF and the corresponding offer in Adobe Target for further synchronization or updates to the offer. We usually save this reference ID in the content or metadata node of the page/asset.
Now, the problem is, what if I copy/paste the asset/page in AEM for reuse? The metadata or properties also get pasted in the newly created page/asset. Now we have two assets/pages with same reference ID, which is problematic while synchronizing. We can solve this by removing the reference ID from the pasted asset/page. But, how do we remove the reference ID automatically when someone copy/pastes the asset/page? How can we listen to copy/paste event in AEM? Is there an event like that in AEM? Read further to get all your answers.
What happens when an asset/page is copied?
When an asset/page is copy/pasted in AEM through Touch UI, the Page Manager API internally sets the event user data in JCR Observation Manager as “changedByPageManagerCopy”. Now you might think, does this happen for page or asset? Yes, this event user data is set on copy/paste of page and asset. This event user data is used by multiple workflow launchers for excluding the launcher. Here, we are going to make use of this event user data in JCR Event Listener for post processing or handling.
Event Listener Implementation
The copy/paste of an asset/page triggers the “NODE_ADDED” JCR event. This event has to be listened and an additional check has to be done based on the event user data to make sure the node is added due to copy/paste and not due to any other operation (because even creation or moving a node will trigger “NODE_ADDED” JCR event). The handling of the event can be delegated to a Sling Job which does further processing (Eg: removal of reference ID). The delegation to sling job is to make sure the processing is guaranteed (Recommended in AEM as Cloud Service). Also, we use the JackrabbitEventFilter to make sure this works fine in a clustered environment as well.
package com.aemks.core.listeners;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.wcm.api.NameConstants;
import org.apache.jackrabbit.api.observation.JackrabbitEventFilter;
import org.apache.jackrabbit.api.observation.JackrabbitObservationManager;
import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.jcr.api.SlingRepository;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import java.util.HashMap;
/**
* This is a JCR event listener which listens to
* copy/paste event in Touch UI.
* (event = NODE_ADDED && event-user-data=changedByPageManagerCopy)
*/
@Component(service = EventListener.class, immediate = true)
public class CopyPasteEventListener implements EventListener {
private SlingRepository repository;
private JobManager jobManager;
private final Logger logger = LoggerFactory.getLogger(getClass());
private Session session; // NOSONAR
private JackrabbitObservationManager observationManager;
public static final String CHANGED_BY_PAGE_MANAGER_COPY = "changedByPageManagerCopy";
public static final String EVENT_HANDLER_JOB="com/aemks/core/jobs/handleCopyPasteEvent";
private final String[] nodeTypes = new String[] { NameConstants.NT_PAGE, JcrConstants.NT_UNSTRUCTURED };
/**
* The event handler delegates the event handling to
* a sling job.
* eventIterator the events iterator
*/
public void onEvent(EventIterator eventIterator) {
while (eventIterator.hasNext()) {
Event event = eventIterator.nextEvent();
String path = null;
try {
path = event.getPath();
if (CHANGED_BY_PAGE_MANAGER_COPY.equalsIgnoreCase(event.getUserData())) {
logger.info("Found the page event path {} triggered by {}", event.getPath(),
CHANGED_BY_PAGE_MANAGER_COPY);
HashMap<String, Object> jobProps = new HashMap<>();
jobProps.put("eventPath", path);
jobManager.addJob(EVENT_HANDLER_JOB, jobProps);
break;
}
} catch (RepositoryException e) {
logger.error("An error occurred while getting event path",e);
}
}
}
/**
* Login using the service user and register the event listener
* for the appropriate content path, node types.
*/
protected void activate() {
try {
session = repository.loginService("listener-service", null);
JackrabbitEventFilter jackrabbitEventFilter = new JackrabbitEventFilter().setAbsPath("/content/aemks")
.setNodeTypes(nodeTypes).setEventTypes(Event.NODE_ADDED).setIsDeep(true).setNoExternal(true)
.setNoLocal(false);
Workspace workSpace = session.getWorkspace();
if (null != workSpace) {
observationManager = (JackrabbitObservationManager) workSpace.getObservationManager();
observationManager.addEventListener(this, jackrabbitEventFilter);
logger.info("The Page Event Listener is Registered at {} for the event type {}.", "/content/aemks",
Event.NODE_ADDED);
}
} catch (RepositoryException e) {
logger.error("An error occurred while getting session",e);
}
}
/**
* On deactivate, close/logout the long running session.
*/
protected void deactivate() {
try {
if (null != observationManager) {
observationManager.removeEventListener(this);
logger.info("The Page Event Listener is removed.");
}
} catch (RepositoryException e) {
logger.error("An error occurred while removing event listener",e);
} finally {
if (null != session) {
session.logout();
}
}
}
}
The Copy Paste Event Listener (JCR Based)
The CopyPasteEventListener.java hosted by GitHub. View raw file.
package com.aemks.core.jobs;
import org.apache.sling.event.jobs.Job;
import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = JobConsumer.class,
immediate = true,
property = { JobConsumer.PROPERTY_TOPICS + "=com/aemks/core/jobs/handleCopyPasteEvent" })
public class CopyPasteEventHandlerJob implements JobConsumer {
private final Logger logger = LoggerFactory.getLogger(getClass());
public JobResult process(Job job) {
String eventPath = job.getProperty("eventPath", String.class);
logger.info("Handling event for path {}",eventPath);
/*
* Do all your processing or handling here
* (Eg: removing the synced metadata like reference ID or Offer ID)
*/
return JobResult.OK;
}
}
The Sling job for handling the event.
CopyPasteEventHandlerJob.java hosted by GitHub. View the raw file.
Finally, do not forget to create the service user for “listener-service” and add relevant permissions (you’ll probably need read permissions to the parent node under which the event has to be listened). A service user can be created for handling the event. Relevant permissions can be applied (probably deleting the metadata or page properties). In AEM Cloud, Sling RepoInit can be used to create the service user and assign relevant permissions.
Key notes
- As per AEM Cloud development guidelines — with everything that is asynchronously happening like acting on observation events, it cannot be guaranteed to be executed on the instance and therefore must be used with care. Hence it is recommended to test thoroughly if you are using AEM Cloud. This approach is currently not tested in AEM Cloud.
- Appropriate permissions have to be provided to the service user to do post processing activity or event handling.
- If this event has to be handled only in author, it is recommended to turn this off in Publish instance.
- Separate event listener can be registered for page and asset.
- This does not work for copy/paste via crx/de.
References
JCR Observation — https://docs.adobe.com/content/docs/en/spec/jcr/2.0/12_Observation.html
JCR Observation in clustered AEM instances -https://cqdump.joerghoh.de/2016/10/06/jcr-observation-in-clustered-aem-instances/
Sling Job — https://sling.apache.org/documentation/bundles/apache-sling-eventing-and-job-handling.html
Exporting XF to Adobe Target — https://experienceleague.adobe.com/docs/experience-manager-65/administering/integration/experience-f....
Sling Repo Init — https://experienceleague.adobe.com/docs/experience-manager-cloud-service/implementing/developing/aem...
Originally published: Jul 13, 2021