Hi @tushaar_srivastava ,
Hope below code is helpful for you.
package com.myproject.aem.core.timeline;
import com.day.cq.dam.api.Asset;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.*;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.json.JSONArray;
import org.json.JSONObject;
import org.osgi.service.component.annotations.*;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Servlet;
import java.io.IOException;
import java.util.*;
/**
* ===============================================================
* 💡 AEM 6.5 Enhanced Asset Timeline Implementation
* ---------------------------------------------------------------
* PART 1: Custom Event Listener → Captures user actions on assets
* PART 2: Custom Servlet → Exposes timeline data as JSON for UI
* ===============================================================
*/
public class EnhancedAssetTimeline {
// ============================================================
// 🧱 PART 1: Custom Event Listener — Tracks Asset Activities
// ============================================================
@Component(
service = EventHandler.class,
property = {
EventConstants.EVENT_TOPIC + "=org/apache/sling/api/resource/Resource/CHANGED",
EventConstants.EVENT_TOPIC + "=com/day/cq/replication/ReplicationAction"
}
)
public static class AssetActivityListener implements EventHandler {
private static final Logger log = LoggerFactory.getLogger(AssetActivityListener.class);
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public void handleEvent(Event event) {
String path = (String) event.getProperty("path");
// 👉 Only process DAM assets
if (path == null || !path.startsWith("/content/dam")) {
return;
}
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(
Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "datawriter"))) {
Resource res = resolver.getResource(path);
if (res == null) return;
Asset asset = res.adaptTo(Asset.class);
if (asset == null) return;
Resource metadata = resolver.getResource(asset.getPath() + "/jcr:content/metadata");
if (metadata == null) return;
// Create or reuse custom node under metadata
Resource customTimeline = metadata.getChild("customTimelineData");
if (customTimeline == null) {
customTimeline = resolver.create(metadata, "customTimelineData", Collections.emptyMap());
}
// 🧩 Main Fields: User, Timestamp, Comment, Action Type
String actionType = "Asset Modified";
if (event.getTopic().contains("ReplicationAction")) {
actionType = "Asset Published";
}
String comment = "System auto-logged action for " + actionType;
Map<String, Object> props = new HashMap<>();
props.put("user", resolver.getUserID());
props.put("timestamp", Calendar.getInstance());
props.put("action", actionType);
props.put("comment", comment);
// Create event node
String nodeName = "event-" + System.currentTimeMillis();
resolver.create(customTimeline, nodeName, props);
resolver.commit();
log.info("✅ Added timeline entry: {} by user {}", actionType, resolver.getUserID());
} catch (Exception e) {
log.error("❌ Error creating custom asset timeline entry for {}", path, e);
}
}
}
// ============================================================
// 🧱 PART 2: Custom Servlet — Returns Timeline Data as JSON
// ============================================================
@Component(
service = { Servlet.class },
property = {
"sling.servlet.paths=/bin/custom/assettimeline",
"sling.servlet.methods=GET"
}
)
public static class AssetTimelineServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(AssetTimelineServlet.class);
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
String assetPath = request.getParameter("path");
if (assetPath == null || assetPath.isEmpty()) {
response.setStatus(400);
response.getWriter().write("{\"error\": \"Missing 'path' parameter\"}");
return;
}
JSONArray result = new JSONArray();
ResourceResolver resolver = request.getResourceResolver();
try {
Resource assetRes = resolver.getResource(assetPath);
Asset asset = assetRes != null ? assetRes.adaptTo(Asset.class) : null;
if (asset != null) {
Resource timelineData = resolver.getResource(asset.getPath() + "/jcr:content/metadata/customTimelineData");
if (timelineData != null) {
for (Resource eventRes : timelineData.getChildren()) {
ValueMap vm = eventRes.getValueMap();
JSONObject eventJson = new JSONObject();
eventJson.put("user", vm.get("user", ""));
eventJson.put("action", vm.get("action", ""));
eventJson.put("comment", vm.get("comment", ""));
Calendar cal = vm.get("timestamp", Calendar.class);
eventJson.put("timestamp", cal != null ? cal.getTimeInMillis() : System.currentTimeMillis());
result.put(eventJson);
}
}
}
response.setContentType("application/json");
response.getWriter().write(result.toString());
} catch (Exception e) {
log.error("Error retrieving asset timeline data for {}", assetPath, e);
response.setStatus(500);
response.getWriter().write("{\"error\":\"Internal server error\"}");
}
}
}
}
Thanks & regards,
Vishal