Loading Now

AEM Event Handling at a Glance

aem_events

AEM Event Handling at a Glance

This guide consolidates AEM event-handling approaches with what each mechanism does, when to use them, how to implement, and copy-paste code samples. Designed for day-to-day development and operations.

AEM exposes several event mechanisms, each suited to a different layer:

  • OSGi EventHandler (Event Admin): Publish/consume application/domain events across OSGi components (topic-based).
  • JCR EventListener (Observation): Subscribe to low-level repository changes (node/property
    created/removed/changed) under a path.
  • Sling ResourceChangeListener: Listen to Sling resource-level create/update/delete events with path/glob filters.
  • Sling Jobs (JobManager): Asynchronous job queue with retry & clustering—ideal to offload heavy work from listeners/steps.
  • TransportHandler (Replication): Implement custom delivery channels for activation/deactivation payloads.
  • Workflow Launchers: Declaratively trigger workflow models on repository changes for multi-step business processes.

When to Use Which

EventHandler – Decoupled intra-app communication (domain events).
JCR EventListener – Precise, path-scoped detection of node/property changes.
ResourceChangeListener – Sling-native resource events with glob/path filtering.
Sling Jobs – Offload heavy/Retryable work with backoff and cluster-aware processing.
Workflow Launchers – Business processes (multi-step, approvals) started by repository changes.
TransportHandler – Custom replication transport to external systems (CDN/API/message bus).


1) OSGi Event Admin – EventHandler

What it is: Topic-based pub/sub via OSGi Event Admin. Producers post events with a string topic and a map of properties; consumers register EventHandlers for topics. AEM exposed some OOTB topics like: ReplicationAction.EVENT_TOPIC

Typical use cases:

  • Announce domain events: “preview generated”, “asset validated”, “cache must purge”.
  • Bridge external signals into AEM as events (e.g., from a message broker via a connector).

Implementation (Consumer)

package com.example.aem.events;

import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.osgi.service.component.annotations.Component;
@Component(
    service = EventHandler.class,
    immediate = true,
    property = {
        EventConstants.EVENT_TOPIC + "=com/example/aem/asset/validated",
        EventConstants.EVENT_TOPIC + "=com/example/aem/cache/purge"
    }
)
public class AssetValidatedEventHandler implements EventHandler {
    @Override
    public void handleEvent(Event event) {
        String topic = (String) event.getProperty(EventConstants.EVENT_TOPIC);
        String path = (String) event.getProperty("path");
        if ("com/example/aem/asset/validated".equals(topic)) {
            // TODO: business logic
        } else if ("com/example/aem/cache/purge".equals(topic)) {
            // TODO: cache purge
        }
    }
}

Posting an event (Producer)

package com.example.aem.events;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import java.util.Dictionary;
import java.util.Hashtable;

@Component(service = AssetEventProducer.class)
public class AssetEventProducer {
    @Reference
    private EventAdmin eventAdmin;

    public void publishValidated(String assetPath) {
        Dictionary<String, Object> props = new Hashtable<>();
        props.put("path", assetPath);
        eventAdmin.postEvent(new Event("com/example/aem/asset/validated", props));
    }
}

Tips

  • Keep topics namespaced (com/yourco/app/...) and stable.
  • Use postEvent (async) for most cases; sendEvent (sync) only if necessary.
  • Avoid excessive events; consider Sling Jobs for heavy/Retryable work.

2) JCR Observation – EventListener

What it is: Subscribe to repository changes (node/property added/removed/changed) under a path using JCR Observation.

Typical use cases

  • React the moment a node or binary is created/updated.
  • Maintain derived content (e.g., index nodes or metadata propagation).
package com.example.aem.jcr;

import java.util.Iterator;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;

import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.LoginException;
import org.osgi.service.component.annotations.*;

@Component(service = EventListener.class, immediate = true)
public class DamAssetCreateListener implements EventListener {

    private static final String ROOT = "/content/dam/project";

    @Reference
    private ResourceResolverFactory rrf;

    private ResourceResolver resolver;
    private ObservationManager observationManager;

    @Activate
    protected void activate() throws Exception {
        // Use a service user with minimal privileges
        resolver = rrf.getServiceResourceResolver(
            java.util.Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "dam-listener")
        );
        Session session = resolver.adaptTo(Session.class);

        observationManager = session.getWorkspace().getObservationManager();
        int eventTypes = Event.NODE_ADDED | Event.PROPERTY_CHANGED;

        // deep = true, no UUID filter, no node type filter, no local = true
        observationManager.addEventListener(this, eventTypes, ROOT, true, null, null, true);
    }

    @Deactivate
    protected void deactivate() {
        try {
            if (observationManager != null) {
                observationManager.removeEventListener(this);
            }
        } catch (Exception ignored) {}
        if (resolver != null) {
            resolver.close();
        }
    }

    @Override
    public void onEvent(Iterator<Event> events) {
        events.forEachRemaining(ev -> {
            try {
                String path = ev.getPath();
                int type = ev.getType(); // Event.NODE_ADDED, etc.
                // TODO: business logic (e.g., tag assets, enqueue processing)
            } catch (Exception e) {
                // log and continue
            }
        });
    }
}

Tips

  • Scope the path tightly to avoid a flood of events.
  • Use service users and close the ResourceResolver.
  • For high-level resource events, consider Sling’s ResourceChangeListener (optional), but JCR observation gives exact low-level control.

3) Sling Resource Observation – ResourceChangeListener

What it is: ‘ResourceChangeListener’ is a Sling-level API that listens to resource events (added, changed, removed) across one or more paths, with support for glob patterns and change types. It’s lighter and more Sling-native than raw JCR Observation, and avoids dealing directly with JCR ‘Event’ types.

Typical use cases

  • Listen for content/resource-level changes under specific Sling paths (e.g., ‘/content/dam/project/**’).
  • React to create/update/delete events without needing low-level JCR specifics.
  • Preferable when working primarily with the Sling Resource API.
package com.example.aem.sling;

import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.osgi.service.component.annotations.Component;

import java.util.List;
import java.util.Set;

/**
 * Listens for resource changes under /content/dam/project, using Sling events.
 */
@Component(
    service = ResourceChangeListener.class,
    property = {
        // Path(s) to watch; supports multiple values
        ResourceChangeListener.PATHS + "=/content/dam/project",
        // Use glob patterns if needed:
        // ResourceChangeListener.GLOB_PATHS + "=/content/dam/project/**",
        // Limit change types:
        ResourceChangeListener.CHANGE_TYPES + "=ADDED",
        ResourceChangeListener.CHANGE_TYPES + "=CHANGED",
        ResourceChangeListener.CHANGE_TYPES + "=REMOVED"
    }
)
public class DamResourceChangeListener implements ResourceChangeListener {

    @Override
    public void onChange(List<ResourceChange> changes) {
        for (ResourceChange change : changes) {
            String path = change.getPath();
            ResourceChange.ChangeType type = change.getType();
            // TODO: react to ADDED/CHANGED/REMOVED; offload heavy work to Sling Jobs
        }
    }
}

Tips

  • Prefer ‘GLOB_PATHS’ for fine-grained scoping (e.g., only ‘*/.jpg’).
  • This is Sling-native work with ‘ResourceResolver’ and resources rather than JCR ‘Session’.
  • Keep the ‘onChange’ handler fast – enqueue heavy tasks to Sling Jobs to avoid blocking the event thread.

4) Background Processing & Retry — Sling Jobs (JobManager)

What it is: Sling Jobs provide a built-in asynchronous job queue with retry, deduplication, and distributed processing across a cluster. You can add jobs to a topic and implement a job consumer to process them. Perfect companion for ‘EventListener’ or ‘ResourceChangeListener’ when work is heavy, slow, or may fail transiently.

Typical use cases

  • Offload heavy processing triggered by repository changes (renditions, metadata enrichment, external API calls).
  • Implement retryable tasks with backoff.
  • Decouple listeners from execution; keep listeners thin.

Enqueue a Job (Producer)

package com.example.aem.jobs;

import org.apache.sling.event.jobs.JobManager;
import org.apache.sling.event.jobs.Job;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import java.util.HashMap;
import java.util.Map;

@Component(service = JobProducer.class)
public class JobProducer {

    public static final String TOPIC = "com/example/aem/dam/preview";

    @Reference
    private JobManager jobManager;

    public void enqueuePreviewJob(String assetPath) {
        Map<String, Object> props = new HashMap<>();
        props.put("path", assetPath);
        Job job = jobManager.addJob(TOPIC, props);
        // Optionally log job.getId()
    }
}

Call ‘enqueuePreviewJob(…)’ from your ‘EventListener’ / ‘ResourceChangeListener’ / Workflow step.

Consume a Job (Consumer)

package com.example.aem.jobs;

import org.apache.sling.event.jobs.consumer.JobConsumer;
import org.apache.sling.event.jobs.consumer.JobConsumer.JobResult;
import org.apache.sling.event.jobs.Job;
import org.osgi.service.component.annotations.Component;

/**
 * Consumes preview generation jobs.
 */
@Component(
    service = JobConsumer.class,
    property = {
        JobConsumer.PROPERTY_TOPICS + "=" + JobProducer.TOPIC
    }
)
public class PreviewJobConsumer implements JobConsumer {

    @Override
    public JobResult process(Job job) {
        String path = (String) job.getProperty("path");
        try {
            // TODO: generate preview; long-running allowed
            return JobResult.OK;
        } catch (TransientException t) {
            return JobResult.RETRY;    // will be retried per Sling job configs
        } catch (Exception e) {
            return JobResult.FAILED;   // logged; consider alerting
        }
    }
}

Tips

  • Use topic namespaces (‘com/yourco/…’) to keep job routing clear.
  • Return ‘RETRY’ for transient failures; configure max retries/backoff via OSGi.
  • Jobs are cluster-aware only one consumer runs a given job across instances.

5) Replication – Custom TransportHandler

What it is: AEM replication activates/deactivates content to publish (or other targets). A TransportHandler defines how payloads are delivered. Built‑in HTTP transports cover standard publish instances; a custom handler lets you deliver to non‑standard endpoints (CDN APIs, message queues, partner systems).

Typical use cases

  • Push activation/deactivation to an external API instead of a publish instance.
  • Sign payloads or transform replication content before delivery.
package com.example.aem.replication;

import com.day.cq.replication.TransportHandler;
import com.day.cq.replication.AgentConfig;
import com.day.cq.replication.ReplicationTransaction;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationContent;
import com.day.cq.replication.TransportException;

import org.osgi.service.component.annotations.Component;

/**
 * Delivers replication transactions to a custom HTTP endpoint.
 */
@Component(service = TransportHandler.class)
public class CustomHttpTransportHandler implements TransportHandler {

    /**
     * Decide if this handler should handle the given agent.
     * Common pattern: check scheme or config name.
     */
    @Override
    public boolean canHandle(AgentConfig agentConfig) {
        String uri = agentConfig.getTransportURI(); // e.g., "custom-http://api.example.com"
        return uri != null && uri.startsWith("custom-http:");
    }

    /**
     * Deliver the replication transaction.
     */
    @Override
    public void deliver(ReplicationTransaction tx) throws TransportException {
        try {
            ReplicationActionType type = tx.getAction().getType(); // ACTIVATE/DEACTIVATE/DELETE
            ReplicationContent content = tx.getContent();

            // Read the payload stream (for ACTIVATE) or path (for other actions)
            // InputStream is = content.getInputStream(); // if present

            String path = tx.getAction().getPath();

            // TODO: call your external endpoint (HTTP client), include path, action type,
            // and bytes if needed. Handle retries, timeouts, idempotency, auth.

        } catch (Exception e) {
            throw new TransportException("Delivery failed", e);
        }
    }

    /**
     * Optional: cancel ongoing deliveries (if your handler supports long‑running calls).
     */
    @Override
    public void cancel() {
        // TODO: abort/close resources if needed
    }
}

Setup

  • Create a Replication Agent (Tools > Deployment > Replication > Agents on author).
  • Set the agent’s transport URI (e.g. ‘custom-http://api.example.com’) so ‘canHandle(…)’ selects your handler.
  • Secure with service users, secrets in OSGi configs, and TLS.

Tips

  • Keep delivery idempotent replication may retry on transient failures.
  • Log action type + path for operational insight.
  • For very large payloads, consider streaming and chunking.

6) Workflow Launchers

What they are: A launcher starts a workflow model automatically when repository changes occur (node created/modified/removed) under a path and matching filters. They’re declarative and can be managed by admins – ideal for content processing and editorial flows.

Typical use cases

  • DAM: On asset upload > run “generate renditions & metadata” workflow.
  • Content: On node modified > run “publish request with approvals”.

Create a Workflow Process Step

package com.example.aem.workflow;

import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowSession;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.exec.WorkflowProcess;

import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.*;

@Component(
    service = WorkflowProcess.class,
    property = {
        "process.label=Generate Preview"
    }
)
public class GeneratePreviewProcess implements WorkflowProcess {

    @Reference
    private ResourceResolverFactory rrf;

    @Override
    public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap args)
            throws WorkflowException {
        String payloadPath = workItem.getWorkflowData().getPayload().toString();
        try (ResourceResolver rr = rrf.getServiceResourceResolver(
                java.util.Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "workflow-process"))) {
            // TODO: generate preview for payloadPath
        } catch (Exception e) {
            throw new WorkflowException("Preview generation failed for " + payloadPath, e);
        }
    }
}

Create a Workflow Model

  • Tools > Workflow > Models > Create a model.
  • Add your “Generate Preview” process step.
  • Save & activate the model.

Configure a Launcher

  • Tools > Workflow > Launchers > Create.
  • Event type: e.g., “Node created”.
  • Path: e.g., ‘/content/dam/project(/.*)?’.
  • Condition/filter: e.g., ‘jcr:primaryType=dam:Asset’ or ‘glob=*/.jpg’.
  • Workflow: select your model.
  • Save and enable.

Tips

  • Prefer specific filters to avoid running workflows on unrelated content.
  • Keep steps idempotent; launchers may fire on reprocessing or restore.
  • For heavy jobs, offload processing to Sling Jobs inside the workflow step.

Differences Summarized

Trigger source: EventHandler (app events); JCR EventListener (repository low-level); ResourceChangeListener (resource-level); TransportHandler (replication delivery); Workflow Launcher (declarative process).
Configuration Focus: EventHandler via OSGi properties; EventListener via ObservationManager; ResourceChangeListener via component properties; TransportHandler via replication agent config; Launchers
via admin UI.
Best fit: EventHandler for decoupled business events; EventListener for exact JCR detection; ResourceChangeListener for Sling resource changes; TransportHandler for custom delivery; Launchers for multi-step processes.

Post Comment