/*
* (C) Copyright 2006-2012 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
* Vladimir Pasquier <vpasquier@nuxeo.com>
*
*/
package org.nuxeo.ecm.platform.ec.notification;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.mail.MessagingException;
import javax.mail.SendFailedException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mvel2.PropertyAccessException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoGroup;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.SystemPrincipal;
import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventBundle;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel;
import org.nuxeo.ecm.core.io.download.DownloadService;
import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
import org.nuxeo.ecm.platform.ec.notification.service.NotificationService;
import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper;
import org.nuxeo.ecm.platform.notification.api.Notification;
import org.nuxeo.ecm.platform.url.DocumentViewImpl;
import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
import org.nuxeo.ecm.platform.url.codec.api.DocumentViewCodec;
import org.nuxeo.ecm.platform.usermanager.UserManager;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.api.login.LoginComponent;
public class NotificationEventListener implements PostCommitFilteringEventListener {
private static final Log log = LogFactory.getLog(NotificationEventListener.class);
private static final String CHECK_READ_PERMISSION_PROPERTY = "notification.check.read.permission";
public static final String NOTIFICATION_DOCUMENT_ID_CODEC_NAME = "notificationDocId";
public static final String JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX = "nxdoc";
private UserManager userManager;
private EmailHelper emailHelper = new EmailHelper();
private NotificationService notificationService = NotificationServiceHelper.getNotificationService();
@Override
public boolean acceptEvent(Event event) {
if (notificationService == null) {
return false;
}
return notificationService.getNotificationEventNames().contains(event.getName());
}
@Override
public void handleEvent(EventBundle events) {
if (notificationService == null) {
log.error("Unable to get NotificationService, exiting");
return;
}
boolean processEvents = false;
for (String name : notificationService.getNotificationEventNames()) {
if (events.containsEventName(name)) {
processEvents = true;
break;
}
}
if (!processEvents) {
return;
}
for (Event event : events) {
Boolean block = (Boolean) event.getContext()
.getProperty(NotificationConstants.DISABLE_NOTIFICATION_SERVICE);
if (block != null && block) {
// ignore the event - we are blocked by the caller
continue;
}
List<Notification> notifs = notificationService.getNotificationsForEvents(event.getName());
if (notifs != null && !notifs.isEmpty()) {
handleNotifications(event, notifs);
}
}
}
protected void handleNotifications(Event event, List<Notification> notifs) {
EventContext ctx = event.getContext();
DocumentEventContext docCtx = null;
if (ctx instanceof DocumentEventContext) {
docCtx = (DocumentEventContext) ctx;
} else {
log.warn("Can not handle notification on a event that is not bound to a DocumentEventContext");
return;
}
if (docCtx.getSourceDocument() instanceof ShallowDocumentModel) {
log.trace("Can not handle notification on a event that is bound to a ShallowDocument");
return;
}
CoreSession coreSession = event.getContext().getCoreSession();
Map<String, Serializable> properties = event.getContext().getProperties();
Map<Notification, List<String>> targetUsers = new HashMap<Notification, List<String>>();
for (NotificationListenerVeto veto : notificationService.getNotificationVetos()) {
if (!veto.accept(event)) {
return;
}
}
for (NotificationListenerHook hookListener : notificationService.getListenerHooks()) {
hookListener.handleNotifications(event);
}
gatherConcernedUsersForDocument(coreSession, docCtx.getSourceDocument(), notifs, targetUsers);
for (Notification notif : targetUsers.keySet()) {
if (!notif.getAutoSubscribed()) {
for (String user : targetUsers.get(notif)) {
sendNotificationSignalForUser(notif, user, event, docCtx);
}
} else {
Object recipientProperty = properties.get(NotificationConstants.RECIPIENTS_KEY);
String[] recipients = null;
if (recipientProperty != null) {
if (recipientProperty instanceof String[]) {
recipients = (String[]) properties.get(NotificationConstants.RECIPIENTS_KEY);
} else if (recipientProperty instanceof String) {
recipients = new String[1];
recipients[0] = (String) recipientProperty;
}
}
if (recipients == null) {
continue;
}
Set<String> users = new HashSet<String>();
for (String recipient : recipients) {
if (recipient == null) {
continue;
}
if (recipient.contains(NuxeoPrincipal.PREFIX)) {
users.add(recipient.replace(NuxeoPrincipal.PREFIX, ""));
} else if (recipient.contains(NuxeoGroup.PREFIX)) {
List<String> groupMembers = getGroupMembers(recipient.replace(NuxeoGroup.PREFIX, ""));
for (String member : groupMembers) {
users.add(member);
}
} else {
// test if the unprefixed recipient corresponds to a
// group, to fetch its members
if (NotificationServiceHelper.getUsersService().getGroup(recipient) != null) {
users.addAll(getGroupMembers(recipient));
} else {
users.add(recipient);
}
}
}
for (String user : users) {
sendNotificationSignalForUser(notif, user, event, docCtx);
}
}
}
}
protected UserManager getUserManager() {
if (userManager == null) {
userManager = Framework.getService(UserManager.class);
}
return userManager;
}
protected List<String> getGroupMembers(String groupId) {
return getUserManager().getUsersInGroupAndSubGroups(groupId);
}
protected void sendNotificationSignalForUser(Notification notification, String subscriptor, Event event,
DocumentEventContext ctx) {
Principal principal;
if (LoginComponent.SYSTEM_USERNAME.equals(subscriptor)) {
principal = new SystemPrincipal(null);
} else {
principal = getUserManager().getPrincipal(subscriptor);
if (principal == null) {
log.error("No Nuxeo principal found for '" + subscriptor
+ "'. No notification will be sent to this user");
return;
}
}
if (Boolean.parseBoolean(Framework.getProperty(CHECK_READ_PERMISSION_PROPERTY))) {
if (!ctx.getCoreSession().hasPermission(principal, ctx.getSourceDocument().getRef(),
SecurityConstants.READ)) {
log.debug("Notification will not be sent: + '" + subscriptor
+ "' do not have Read permission on document " + ctx.getSourceDocument().getId());
return;
}
}
log.debug("Producing notification message.");
Map<String, Serializable> eventInfo = ctx.getProperties();
DocumentModel doc = ctx.getSourceDocument();
String author = ctx.getPrincipal().getName();
Calendar created = (Calendar) ctx.getSourceDocument().getPropertyValue("dc:created");
// Get notification document codec
DocumentViewCodecManager codecService = Framework.getService(DocumentViewCodecManager.class);
DocumentViewCodec codec = codecService.getCodec(NOTIFICATION_DOCUMENT_ID_CODEC_NAME);
boolean isNotificationCodec = codec != null;
boolean isJSFUI = isNotificationCodec && JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX.equals(codec.getPrefix());
eventInfo.put(NotificationConstants.IS_JSF_UI, isJSFUI);
eventInfo.put(NotificationConstants.DESTINATION_KEY, subscriptor);
eventInfo.put(NotificationConstants.NOTIFICATION_KEY, notification);
eventInfo.put(NotificationConstants.DOCUMENT_ID_KEY, doc.getId());
eventInfo.put(NotificationConstants.DATE_TIME_KEY, new Date(event.getTime()));
eventInfo.put(NotificationConstants.AUTHOR_KEY, author);
eventInfo.put(NotificationConstants.DOCUMENT_VERSION, doc.getVersionLabel());
eventInfo.put(NotificationConstants.DOCUMENT_STATE, doc.getCurrentLifeCycleState());
eventInfo.put(NotificationConstants.DOCUMENT_CREATED, created.getTime());
if (isNotificationCodec) {
StringBuilder userUrl = new StringBuilder();
userUrl.append(notificationService.getServerUrlPrefix());
if (!isJSFUI) {
userUrl.append("ui/");
userUrl.append("#!/");
}
userUrl.append("user/").append(ctx.getPrincipal().getName());
eventInfo.put(NotificationConstants.USER_URL_KEY, userUrl.toString());
}
eventInfo.put(NotificationConstants.DOCUMENT_LOCATION, doc.getPathAsString());
// Main file link for downloading
BlobHolder bh = doc.getAdapter(BlobHolder.class);
if (bh != null && bh.getBlob() != null) {
DownloadService downloadService = Framework.getService(DownloadService.class);
String filename = bh.getBlob().getFilename();
String docMainFile = notificationService.getServerUrlPrefix()
+ downloadService.getDownloadUrl(doc, DownloadService.BLOBHOLDER_0, filename);
eventInfo.put(NotificationConstants.DOCUMENT_MAIN_FILE, docMainFile);
}
if (!isDeleteEvent(event.getName())) {
if (isNotificationCodec) {
eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY,
codecService.getUrlFromDocumentView(NOTIFICATION_DOCUMENT_ID_CODEC_NAME,
new DocumentViewImpl(doc), true, notificationService.getServerUrlPrefix()));
}
eventInfo.put(NotificationConstants.DOCUMENT_TITLE_KEY, doc.getTitle());
}
if (isInterestedInNotification(notification)) {
sendNotification(event, ctx);
if (log.isDebugEnabled()) {
log.debug("notification " + notification.getName() + " sent to " + notification.getSubject());
}
}
}
public void sendNotification(Event event, DocumentEventContext ctx) {
String eventId = event.getName();
log.debug("Received a message for notification sender with eventId : " + eventId);
Map<String, Serializable> eventInfo = ctx.getProperties();
String userDest = (String) eventInfo.get(NotificationConstants.DESTINATION_KEY);
NotificationImpl notif = (NotificationImpl) eventInfo.get(NotificationConstants.NOTIFICATION_KEY);
// send email
NuxeoPrincipal recepient = NotificationServiceHelper.getUsersService().getPrincipal(userDest);
if (recepient == null) {
log.error("Couldn't find user: " + userDest + " to send her a mail.");
return;
}
String email = recepient.getEmail();
if (email == null || "".equals(email)) {
log.error("No email found for user: " + userDest);
return;
}
String subjectTemplate = notif.getSubjectTemplate();
String mailTemplate = null;
// mail template can be dynamically computed from a MVEL expression
if (notif.getTemplateExpr() != null) {
try {
mailTemplate = emailHelper.evaluateMvelExpresssion(notif.getTemplateExpr(), eventInfo);
} catch (PropertyAccessException pae) {
if (log.isDebugEnabled()) {
log.debug("Cannot evaluate mail template expression '" + notif.getTemplateExpr()
+ "' in that context " + eventInfo, pae);
}
}
}
// if there is no mailTemplate evaluated, use the defined one
if (StringUtils.isEmpty(mailTemplate)) {
mailTemplate = notif.getTemplate();
}
log.debug("email: " + email);
log.debug("mail template: " + mailTemplate);
log.debug("subject template: " + subjectTemplate);
Map<String, Object> mail = new HashMap<String, Object>();
mail.put("mail.to", email);
String authorUsername = (String) eventInfo.get(NotificationConstants.AUTHOR_KEY);
if (authorUsername != null) {
NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
mail.put(NotificationConstants.PRINCIPAL_AUTHOR_KEY, author);
}
mail.put(NotificationConstants.DOCUMENT_KEY, ctx.getSourceDocument());
String subject = notif.getSubject() == null ? NotificationConstants.NOTIFICATION_KEY : notif.getSubject();
subject = notificationService.getEMailSubjectPrefix() + subject;
mail.put("subject", subject);
mail.put("template", mailTemplate);
mail.put("subjectTemplate", subjectTemplate);
// Transferring all data from event to email
for (String key : eventInfo.keySet()) {
mail.put(key, eventInfo.get(key) == null ? "" : eventInfo.get(key));
log.debug("Mail prop: " + key);
}
mail.put(NotificationConstants.EVENT_ID_KEY, eventId);
try {
emailHelper.sendmail(mail);
} catch (MessagingException e) {
String cause = "";
if ((e instanceof SendFailedException) && (e.getCause() instanceof SendFailedException)) {
cause = " - Cause: " + e.getCause().getMessage();
}
log.warn("Failed to send notification email to '" + email + "': " + e.getClass().getName() + ": "
+ e.getMessage() + cause);
}
}
/**
* Adds the concerned users to the list of targeted users for these notifications.
*/
private void gatherConcernedUsersForDocument(CoreSession coreSession, DocumentModel doc, List<Notification> notifs,
Map<Notification, List<String>> targetUsers) {
if (doc.getPath().segmentCount() > 1) {
log.debug("Searching document: " + doc.getName());
getInterstedUsers(doc, notifs, targetUsers);
if (doc.getParentRef() != null && coreSession.exists(doc.getParentRef())) {
DocumentModel parent = getDocumentParent(coreSession, doc);
gatherConcernedUsersForDocument(coreSession, parent, notifs, targetUsers);
}
}
}
private DocumentModel getDocumentParent(CoreSession coreSession, DocumentModel doc) {
if (doc == null) {
return null;
}
return coreSession.getDocument(doc.getParentRef());
}
private void getInterstedUsers(DocumentModel doc, List<Notification> notifs,
Map<Notification, List<String>> targetUsers) {
for (Notification notification : notifs) {
if (!notification.getAutoSubscribed()) {
List<String> userGroup = notificationService.getSubscribers(notification.getName(), doc);
for (String subscriptor : userGroup) {
if (subscriptor != null) {
if (isUser(subscriptor)) {
storeUserForNotification(notification, subscriptor.substring(5), targetUsers);
} else {
// it is a group - get all users and send
// notifications to them
List<String> usersOfGroup = getGroupMembers(subscriptor.substring(6));
if (usersOfGroup != null && !usersOfGroup.isEmpty()) {
for (String usr : usersOfGroup) {
storeUserForNotification(notification, usr, targetUsers);
}
}
}
}
}
} else {
// An automatic notification happens
// should be sent to interested users
targetUsers.put(notification, new ArrayList<String>());
}
}
}
private static void storeUserForNotification(Notification notification, String user,
Map<Notification, List<String>> targetUsers) {
List<String> subscribedUsers = targetUsers.get(notification);
if (subscribedUsers == null) {
targetUsers.put(notification, new ArrayList<String>());
}
if (!targetUsers.get(notification).contains(user)) {
targetUsers.get(notification).add(user);
}
}
private boolean isDeleteEvent(String eventId) {
List<String> deletionEvents = new ArrayList<String>();
deletionEvents.add("aboutToRemove");
deletionEvents.add("documentRemoved");
return deletionEvents.contains(eventId);
}
private boolean isUser(String subscriptor) {
return subscriptor != null && subscriptor.startsWith("user:");
}
public boolean isInterestedInNotification(Notification notif) {
return notif != null && "email".equals(notif.getChannel());
}
public EmailHelper getEmailHelper() {
return emailHelper;
}
public void setEmailHelper(EmailHelper emailHelper) {
this.emailHelper = emailHelper;
}
}