AEM Cloud Service - Watch Folders for Asset Uploads Send Inbox Notifications

Goal

With AEM Assets Essentials an author can watch folders and get notifications when other users upload assets to watched folder. This post is for something similar on Cloud Services Assets, to configure and Receive Inbox Notifications on Create and Remove Assets in Specific Folders

Demo | Package Install | Github


Watch Folder


Inbox Notifications


Asset Created Notification


Asset Removed Notification


Solution

1) Add a Sling Post Processor apps.eaem.assets.core.listeners.WatchFolderSaveProcessor to save Watch configuration (added to Folder Properties in next steps..)

package apps.eaem.assets.core.listeners;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.SlingPostProcessor;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.*;
import java.util.List;

@Component(
immediate = true,
service = { SlingPostProcessor.class },
property = {
Constants.SERVICE_RANKING + ":Integer=-99",
}
)
public class WatchFolderSaveProcessor implements SlingPostProcessor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

private static final String OPERATION = ":operation";
private static final String OPERATION_DAM_SHARE_FOLDER = "dam.share.folder";
public static final String WATCH_FOLDER = "eaemWatch";

@Override
public void process(final SlingHttpServletRequest request, final List<Modification> modifications){
try {
final String operation = request.getParameter(OPERATION);

if ( operation == null || !operation.equals(OPERATION_DAM_SHARE_FOLDER)) {
return;
}

final ResourceResolver resolver = request.getResourceResolver();
final String watchFolder = request.getParameter(WATCH_FOLDER);

if (!"true".equals(watchFolder)) {
return;
}

Session session = resolver.adaptTo(Session.class);
ValueFactory valueFactory = session.getValueFactory();

final String folderPath = request.getPathInfo();
Node jcrContent = resolver.getResource(folderPath).getChild("jcr:content").adaptTo(Node.class);
Property watchProp = null;

if(jcrContent.hasProperty(WATCH_FOLDER)){
watchProp = jcrContent.getProperty(WATCH_FOLDER);
}

Value thisUser = valueFactory.createValue(session.getUserID());
Value values[] = ((watchProp == null) ? new Value[] { thisUser } : ArrayUtils.add(watchProp.getValues(), thisUser));

jcrContent.setProperty(WATCH_FOLDER, values);
} catch (Exception e) {
logger.error("Error saving folder watch", e);
}
}
}


2) Add a Sling Observation Listener apps.eaem.assets.core.listeners.AssetsAddedInFolderListener to process Asset events like Create and Remove and send Inbox Notifications. For remove, since the asset does not exist during event processing, the remove specific information can be obtained from Audit history /var/audit/com.day.cq.dam

package apps.eaem.assets.core.listeners;

import com.adobe.granite.taskmanagement.Task;
import com.adobe.granite.taskmanagement.TaskManager;
import com.adobe.granite.workflow.exec.InboxItem;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.*;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
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.*;
import java.util.*;

import static apps.eaem.assets.core.listeners.WatchFolderSaveProcessor.WATCH_FOLDER;
import static org.apache.sling.api.resource.observation.ResourceChangeListener.*;

@Component(
service = ResourceChangeListener.class,
immediate = true,
property = {
CHANGES + "=" + CHANGE_ADDED,
CHANGES + "=" + CHANGE_REMOVED,
PATHS + "=glob:/content/dam"
})
public class AssetsAddedInFolderListener implements ResourceChangeListener {
private final Logger logger = LoggerFactory.getLogger(getClass());

private static final String EAEM_SERVICE_USER = "eaem-service-user";
private static final String ORIGINAL_RENDITION_PATH = "/jcr:content/renditions/original";
public static final String NOTIFICATION_TASK_TYPE = "Notification";
private static final String AUDTT_PATH = "/var/audit/com.day.cq.dam";

@Reference
private ResourceResolverFactory resolverFactory;

public void onChange(List<ResourceChange> changes) {
String assetPath = null;
Iterator<ResourceChange> changesItr = changes.iterator();
ResourceChange change = null;
ResourceResolver resolver = getServiceResourceResolver(resolverFactory);

while(changesItr.hasNext()){
change = changesItr.next();

if(change.getType() == ResourceChange.ChangeType.REMOVED){
assetPath = change.getPath();
break;
}else if(change.getPath().endsWith(ORIGINAL_RENDITION_PATH)) {
assetPath = change.getPath();
assetPath = assetPath.substring(0, assetPath.indexOf(ORIGINAL_RENDITION_PATH));
break;
}
};

if(StringUtils.isEmpty(assetPath)){
return;
}

String parentPath = assetPath.substring(0, assetPath.lastIndexOf("/"));

try{
Resource parentFolder = resolver.getResource(parentPath);
Node jcrContent = parentFolder.getChild("jcr:content").adaptTo(Node.class);

if(!jcrContent.hasProperty(WATCH_FOLDER)){
return;
}

Property watchProp = jcrContent.getProperty(WATCH_FOLDER);
Value values[] = watchProp.getValues();

for(Value v : values){
if(change.getType() == ResourceChange.ChangeType.REMOVED){
createAssetRemovedNotification(resolver, assetPath, v.getString());
}else if(change.getType() == ResourceChange.ChangeType.ADDED){
createAssetAddedNotification(resolver.getResource(assetPath), v.getString());
}
}
}catch (Exception ex) {
logger.error("Error Creating Task", ex);
}
}

private Task createAssetRemovedNotification(ResourceResolver resolver, String resourcePath, String assignee) throws Exception{
TaskManager taskManager = resolver.adaptTo(TaskManager.class);
Task task = taskManager.getTaskManagerFactory().newTask(NOTIFICATION_TASK_TYPE);
String assetName = resourcePath.substring(resourcePath.lastIndexOf("/") + 1);
String folderPath = resourcePath.substring(0, resourcePath.lastIndexOf("/"));

StringBuilder description = new StringBuilder();
description.append("Asset '" + assetName + "' removed. ");
description.append("You are receiving this notification because you have watch enabled on the folder.\r\n");
description.append("Name: ").append(assetName).append("\r\n");
description.append("Folder: ").append(folderPath).append("\r\n");
description.append("Removed By: ").append(getRemovedByUserIdFromAudit(resolver, resourcePath)).append("\r\n");
description.append("Removed Date: ").append(new Date());

task.setName(assetName);
task.setContentPath(resourcePath);
task.setDescription( description.toString() );
task.setCurrentAssignee(assignee);
task.setPriority(InboxItem.Priority.LOW);

taskManager.createTask(task);

return task;
}

private Task createAssetAddedNotification(Resource resource, String assignee) throws Exception{
ResourceResolver resolver = resource.getResourceResolver();
Node assetNode = resource.adaptTo(Node.class);

TaskManager taskManager = resolver.adaptTo(TaskManager.class);
Task task = taskManager.getTaskManagerFactory().newTask(NOTIFICATION_TASK_TYPE);

StringBuilder description = new StringBuilder();
description.append("Asset '" + resource.getName() + "' added. ");
description.append("You are receiving this notification because you have watch enabled on the folder.\r\n");
description.append("Name: ").append(resource.getName()).append("\r\n");
description.append("Folder: ").append(resource.getParent().getPath()).append("\r\n");
description.append("Created By: ").append(assetNode.getProperty("jcr:createdBy").getString()).append("\r\n");
description.append("Created Date: ").append(assetNode.getProperty("jcr:created").getValue().getDate().getTime());

task.setName(resource.getName());
task.setContentPath(resource.getPath());
task.setDescription(description.toString());
task.setCurrentAssignee(assignee);
task.setPriority(InboxItem.Priority.LOW);

taskManager.createTask(task);

return task;
}

private String getRemovedByUserIdFromAudit(ResourceResolver resolver, String resourcePath){
String removedBy = "";
Resource auditHistoryResource = resolver.getResource(AUDTT_PATH + resourcePath);

if(auditHistoryResource == null) {
return removedBy;
}

Iterator<Resource> auditIter = auditHistoryResource.getChildren().iterator();
Resource auditResource = null;
ValueMap auditVM = null;

while(auditIter.hasNext()){
auditResource = auditIter.next();
auditVM = auditResource.getValueMap();

if("ASSET_REMOVED".equals(auditVM.get("cq:type"))){
removedBy = auditVM.get("cq:userid","");
break;
}
}

return removedBy;
}

private ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
Map<String, Object> subServiceUser = new HashMap<>();
subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
try {
return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
} catch (LoginException ex) {
logger.error("Could not login as SubService user {}", EAEM_SERVICE_USER, ex);
return null;
}
}
}


3) Add a RepoInit config \ui.config\src\main\content\jcr_root\apps\eaem-watch-folders-inbox-notify\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config for service user reading /content/dam and /var/audit nodes

scripts=[
"
create service user eaem-service-user with path system/experience-aem
set ACL for eaem-service-user
allow jcr:all on /content/dam
allow jcr:all on /var/audit
end
"
]


4) Add the bundle to service user mapping in ui.config\src\main\content\jcr_root\apps\eaem-watch-folders-inbox-notify\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~e.cfg.json

{
"user.mapping": [
"eaem-watch-folders-inbox-notify.core:eaem-service-user=[eaem-service-user]"
]
}


5) Add a clientlib /apps/eaem-watch-folders-inbox-notify/clientlibs/clientlib-watch-folders with categories=cq.gui.damadmin.v2.foldershare.coral and JS code in /apps/eaem-watch-folders-inbox-notify/clientlibs/clientlib-watch-folders/watch-folder.js for Watch checkbox

(function ($, $document) {
"use strict";

const METADATA_EDITOR_PAGE = "/mnt/overlay/dam/gui/content/assets/v2/foldersharewizard.html",
EAEM_WATCH = "eaemWatch",
ORDERABLE_SEL ="#orderable";

if (!isFolderProperties()) {
return;
}

$document.on("foundation-contentloaded", addWatchFolder);

function addWatchFolder(){
const locPath = window.location.pathname,
folderPath = locPath.substring(locPath.indexOf(METADATA_EDITOR_PAGE) + METADATA_EDITOR_PAGE.length) + "/jcr:content.json";

$.ajax(folderPath).done((folderJson) => {
let eaemWatch = folderJson[EAEM_WATCH] || [];
$("form").find(ORDERABLE_SEL).after(getWatchCheckBox(eaemWatch && eaemWatch.includes(getLoggedInUserID())));
})
}

function getWatchCheckBox(isWatching){
const checked = isWatching ? " checked " : " ";
return '<coral-checkbox ' + checked + ' name="' + EAEM_WATCH + '" value="true" class="coral-Form-field _coral-Checkbox">Watch</coral-checkbox>';
}

function isFolderProperties() {
return (window.location.pathname.indexOf(METADATA_EDITOR_PAGE) >= 0);
}

function getLoggedInUserID(){
return window.sessionStorage.getItem("granite.shell.badge.user");
}
}(jQuery, jQuery(document)));


No comments:

Post a Comment