001/* 002 * (C) Copyright 2006-2012 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * Vladimir Pasquier <vpasquier@nuxeo.com> 017 * 018 */ 019 020package org.nuxeo.ecm.platform.ec.notification; 021 022import java.io.Serializable; 023import java.security.Principal; 024import java.util.ArrayList; 025import java.util.Calendar; 026import java.util.Date; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032 033import javax.mail.MessagingException; 034import javax.mail.SendFailedException; 035 036import org.apache.commons.lang.StringUtils; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.mvel2.PropertyAccessException; 040import org.nuxeo.ecm.core.api.CoreSession; 041import org.nuxeo.ecm.core.api.DataModel; 042import org.nuxeo.ecm.core.api.DocumentModel; 043import org.nuxeo.ecm.core.api.NuxeoGroup; 044import org.nuxeo.ecm.core.api.NuxeoPrincipal; 045import org.nuxeo.ecm.core.api.SystemPrincipal; 046import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 047import org.nuxeo.ecm.core.api.security.SecurityConstants; 048import org.nuxeo.ecm.core.event.Event; 049import org.nuxeo.ecm.core.event.EventBundle; 050import org.nuxeo.ecm.core.event.EventContext; 051import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener; 052import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 053import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel; 054import org.nuxeo.ecm.core.io.download.DownloadService; 055import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper; 056import org.nuxeo.ecm.platform.ec.notification.service.NotificationService; 057import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 058import org.nuxeo.ecm.platform.notification.api.Notification; 059import org.nuxeo.ecm.platform.url.DocumentViewImpl; 060import org.nuxeo.ecm.platform.url.api.DocumentView; 061import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager; 062import org.nuxeo.ecm.platform.usermanager.UserManager; 063import org.nuxeo.runtime.api.Framework; 064import org.nuxeo.runtime.api.login.LoginComponent; 065 066public class NotificationEventListener implements PostCommitFilteringEventListener { 067 068 private static final Log log = LogFactory.getLog(NotificationEventListener.class); 069 070 private static final String CHECK_READ_PERMISSION_PROPERTY = "notification.check.read.permission"; 071 072 private DocumentViewCodecManager docLocator; 073 074 private UserManager userManager; 075 076 private EmailHelper emailHelper = new EmailHelper(); 077 078 private NotificationService notificationService = NotificationServiceHelper.getNotificationService(); 079 080 @Override 081 public boolean acceptEvent(Event event) { 082 if (notificationService == null) { 083 return false; 084 } 085 return notificationService.getNotificationEventNames().contains(event.getName()); 086 } 087 088 @Override 089 public void handleEvent(EventBundle events) { 090 091 if (notificationService == null) { 092 log.error("Unable to get NotificationService, exiting"); 093 return; 094 } 095 boolean processEvents = false; 096 for (String name : notificationService.getNotificationEventNames()) { 097 if (events.containsEventName(name)) { 098 processEvents = true; 099 break; 100 } 101 } 102 if (!processEvents) { 103 return; 104 } 105 for (Event event : events) { 106 Boolean block = (Boolean) event.getContext().getProperty(NotificationConstants.DISABLE_NOTIFICATION_SERVICE); 107 if (block != null && block) { 108 // ignore the event - we are blocked by the caller 109 continue; 110 } 111 List<Notification> notifs = notificationService.getNotificationsForEvents(event.getName()); 112 if (notifs != null && !notifs.isEmpty()) { 113 handleNotifications(event, notifs); 114 } 115 } 116 117 } 118 119 protected void handleNotifications(Event event, List<Notification> notifs) { 120 121 EventContext ctx = event.getContext(); 122 DocumentEventContext docCtx = null; 123 if (ctx instanceof DocumentEventContext) { 124 docCtx = (DocumentEventContext) ctx; 125 } else { 126 log.warn("Can not handle notification on a event that is not bound to a DocumentEventContext"); 127 return; 128 } 129 130 if(docCtx.getSourceDocument() instanceof ShallowDocumentModel) { 131 log.trace("Can not handle notification on a event that is bound to a ShallowDocument"); 132 return; 133 } 134 135 CoreSession coreSession = event.getContext().getCoreSession(); 136 Map<String, Serializable> properties = event.getContext().getProperties(); 137 Map<Notification, List<String>> targetUsers = new HashMap<Notification, List<String>>(); 138 139 for (NotificationListenerVeto veto : notificationService.getNotificationVetos()) { 140 if (!veto.accept(event)) { 141 return; 142 } 143 } 144 145 for (NotificationListenerHook hookListener : notificationService.getListenerHooks()) { 146 hookListener.handleNotifications(event); 147 } 148 149 gatherConcernedUsersForDocument(coreSession, docCtx.getSourceDocument(), notifs, targetUsers); 150 151 for (Notification notif : targetUsers.keySet()) { 152 if (!notif.getAutoSubscribed()) { 153 for (String user : targetUsers.get(notif)) { 154 sendNotificationSignalForUser(notif, user, event, docCtx); 155 } 156 } else { 157 Object recipientProperty = properties.get(NotificationConstants.RECIPIENTS_KEY); 158 String[] recipients = null; 159 if (recipientProperty != null) { 160 if (recipientProperty instanceof String[]) { 161 recipients = (String[]) properties.get(NotificationConstants.RECIPIENTS_KEY); 162 } else if (recipientProperty instanceof String) { 163 recipients = new String[1]; 164 recipients[0] = (String) recipientProperty; 165 } 166 } 167 if (recipients == null) { 168 continue; 169 } 170 Set<String> users = new HashSet<String>(); 171 for (String recipient : recipients) { 172 if (recipient == null) { 173 continue; 174 } 175 if (recipient.contains(NuxeoPrincipal.PREFIX)) { 176 users.add(recipient.replace(NuxeoPrincipal.PREFIX, "")); 177 } else if (recipient.contains(NuxeoGroup.PREFIX)) { 178 List<String> groupMembers = getGroupMembers(recipient.replace(NuxeoGroup.PREFIX, "")); 179 for (String member : groupMembers) { 180 users.add(member); 181 } 182 } else { 183 // test if the unprefixed recipient corresponds to a 184 // group, to fetch its members 185 if (NotificationServiceHelper.getUsersService().getGroup(recipient) != null) { 186 users.addAll(getGroupMembers(recipient)); 187 } else { 188 users.add(recipient); 189 } 190 } 191 192 } 193 for (String user : users) { 194 sendNotificationSignalForUser(notif, user, event, docCtx); 195 } 196 197 } 198 } 199 200 } 201 202 protected UserManager getUserManager() { 203 if (userManager == null) { 204 userManager = Framework.getService(UserManager.class); 205 } 206 return userManager; 207 } 208 209 protected List<String> getGroupMembers(String groupId) { 210 return getUserManager().getUsersInGroupAndSubGroups(groupId); 211 } 212 213 protected void sendNotificationSignalForUser(Notification notification, String subscriptor, Event event, 214 DocumentEventContext ctx) { 215 216 Principal principal; 217 if (LoginComponent.SYSTEM_USERNAME.equals(subscriptor)) { 218 principal = new SystemPrincipal(null); 219 } else { 220 principal = getUserManager().getPrincipal(subscriptor); 221 if (principal == null) { 222 log.error("No Nuxeo principal found for '" + subscriptor 223 + "'. No notification will be sent to this user"); 224 return; 225 } 226 } 227 228 if (Boolean.parseBoolean(Framework.getProperty(CHECK_READ_PERMISSION_PROPERTY))) { 229 if (!ctx.getCoreSession().hasPermission(principal, ctx.getSourceDocument().getRef(), SecurityConstants.READ)) { 230 log.debug("Notification will not be sent: + '" + subscriptor 231 + "' do not have Read permission on document " + ctx.getSourceDocument().getId()); 232 return; 233 } 234 } 235 236 log.debug("Producing notification message."); 237 238 Map<String, Serializable> eventInfo = ctx.getProperties(); 239 DocumentModel doc = ctx.getSourceDocument(); 240 String author = ctx.getPrincipal().getName(); 241 Calendar created = (Calendar) ctx.getSourceDocument().getPropertyValue("dc:created"); 242 243 eventInfo.put(NotificationConstants.DESTINATION_KEY, subscriptor); 244 eventInfo.put(NotificationConstants.NOTIFICATION_KEY, notification); 245 eventInfo.put(NotificationConstants.DOCUMENT_ID_KEY, doc.getId()); 246 eventInfo.put(NotificationConstants.DATE_TIME_KEY, new Date(event.getTime())); 247 eventInfo.put(NotificationConstants.AUTHOR_KEY, author); 248 eventInfo.put(NotificationConstants.DOCUMENT_VERSION, doc.getVersionLabel()); 249 eventInfo.put(NotificationConstants.DOCUMENT_STATE, doc.getCurrentLifeCycleState()); 250 eventInfo.put(NotificationConstants.DOCUMENT_CREATED, created.getTime()); 251 StringBuilder userUrl = new StringBuilder(); 252 userUrl.append(notificationService.getServerUrlPrefix()).append("user/").append(ctx.getPrincipal().getName()); 253 eventInfo.put(NotificationConstants.USER_URL_KEY, userUrl.toString()); 254 eventInfo.put(NotificationConstants.DOCUMENT_LOCATION, doc.getPathAsString()); 255 // Main file link for downloading 256 BlobHolder bh = doc.getAdapter(BlobHolder.class); 257 if (bh != null && bh.getBlob() != null) { 258 DownloadService downloadService = Framework.getService(DownloadService.class); 259 String filename = bh.getBlob().getFilename(); 260 String docMainFile = notificationService.getServerUrlPrefix() 261 + downloadService.getDownloadUrl(doc, DownloadService.BLOBHOLDER_0, filename); 262 eventInfo.put(NotificationConstants.DOCUMENT_MAIN_FILE, docMainFile); 263 } 264 265 if (!isDeleteEvent(event.getName())) { 266 DocumentView docView = new DocumentViewImpl(doc); 267 DocumentViewCodecManager docLocator = getDocLocator(); 268 if (docLocator != null) { 269 eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY, 270 getDocLocator().getUrlFromDocumentView(docView, true, notificationService.getServerUrlPrefix())); 271 } else { 272 eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY, ""); 273 } 274 eventInfo.put(NotificationConstants.DOCUMENT_TITLE_KEY, doc.getTitle()); 275 } 276 277 if (isInterestedInNotification(notification)) { 278 sendNotification(event, ctx); 279 if (log.isDebugEnabled()) { 280 log.debug("notification " + notification.getName() + " sent to " + notification.getSubject()); 281 } 282 } 283 } 284 285 public void sendNotification(Event event, DocumentEventContext ctx) { 286 287 String eventId = event.getName(); 288 log.debug("Received a message for notification sender with eventId : " + eventId); 289 290 Map<String, Serializable> eventInfo = ctx.getProperties(); 291 String userDest = (String) eventInfo.get(NotificationConstants.DESTINATION_KEY); 292 NotificationImpl notif = (NotificationImpl) eventInfo.get(NotificationConstants.NOTIFICATION_KEY); 293 294 // send email 295 NuxeoPrincipal recepient = NotificationServiceHelper.getUsersService().getPrincipal(userDest); 296 if (recepient == null) { 297 log.error("Couldn't find user: " + userDest + " to send her a mail."); 298 return; 299 } 300 // XXX hack, principals have only one model 301 DataModel model = recepient.getModel().getDataModels().values().iterator().next(); 302 String email = (String) model.getData("email"); 303 if (email == null || "".equals(email)) { 304 log.error("No email found for user: " + userDest); 305 return; 306 } 307 308 String subjectTemplate = notif.getSubjectTemplate(); 309 310 String mailTemplate = null; 311 // mail template can be dynamically computed from a MVEL expression 312 if (notif.getTemplateExpr() != null) { 313 try { 314 mailTemplate = emailHelper.evaluateMvelExpresssion(notif.getTemplateExpr(), eventInfo); 315 } catch (PropertyAccessException pae) { 316 if (log.isDebugEnabled()) { 317 log.debug("Cannot evaluate mail template expression '" + notif.getTemplateExpr() 318 + "' in that context " + eventInfo, pae); 319 } 320 } 321 } 322 // if there is no mailTemplate evaluated, use the defined one 323 if (StringUtils.isEmpty(mailTemplate)) { 324 mailTemplate = notif.getTemplate(); 325 } 326 327 log.debug("email: " + email); 328 log.debug("mail template: " + mailTemplate); 329 log.debug("subject template: " + subjectTemplate); 330 331 Map<String, Object> mail = new HashMap<String, Object>(); 332 mail.put("mail.to", email); 333 334 String authorUsername = (String) eventInfo.get(NotificationConstants.AUTHOR_KEY); 335 336 if (authorUsername != null) { 337 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 338 mail.put(NotificationConstants.PRINCIPAL_AUTHOR_KEY, author); 339 } 340 341 mail.put(NotificationConstants.DOCUMENT_KEY, ctx.getSourceDocument()); 342 String subject = notif.getSubject() == null ? NotificationConstants.NOTIFICATION_KEY : notif.getSubject(); 343 subject = notificationService.getEMailSubjectPrefix() + subject; 344 mail.put("subject", subject); 345 mail.put("template", mailTemplate); 346 mail.put("subjectTemplate", subjectTemplate); 347 348 // Transferring all data from event to email 349 for (String key : eventInfo.keySet()) { 350 mail.put(key, eventInfo.get(key) == null ? "" : eventInfo.get(key)); 351 log.debug("Mail prop: " + key); 352 } 353 354 mail.put(NotificationConstants.EVENT_ID_KEY, eventId); 355 356 try { 357 emailHelper.sendmail(mail); 358 } catch (MessagingException e) { 359 String cause = ""; 360 if ((e instanceof SendFailedException) && (e.getCause() instanceof SendFailedException)) { 361 cause = " - Cause: " + e.getCause().getMessage(); 362 } 363 log.warn("Failed to send notification email to '" + email + "': " + e.getClass().getName() + ": " 364 + e.getMessage() + cause); 365 } 366 } 367 368 /** 369 * Adds the concerned users to the list of targeted users for these notifications. 370 */ 371 private void gatherConcernedUsersForDocument(CoreSession coreSession, DocumentModel doc, List<Notification> notifs, 372 Map<Notification, List<String>> targetUsers) { 373 if (doc.getPath().segmentCount() > 1) { 374 log.debug("Searching document: " + doc.getName()); 375 getInterstedUsers(doc, notifs, targetUsers); 376 if (doc.getParentRef() != null && coreSession.exists(doc.getParentRef())) { 377 DocumentModel parent = getDocumentParent(coreSession, doc); 378 gatherConcernedUsersForDocument(coreSession, parent, notifs, targetUsers); 379 } 380 } 381 } 382 383 private DocumentModel getDocumentParent(CoreSession coreSession, DocumentModel doc) { 384 if (doc == null) { 385 return null; 386 } 387 return coreSession.getDocument(doc.getParentRef()); 388 } 389 390 private void getInterstedUsers(DocumentModel doc, List<Notification> notifs, 391 Map<Notification, List<String>> targetUsers) { 392 for (Notification notification : notifs) { 393 if (!notification.getAutoSubscribed()) { 394 List<String> userGroup = notificationService.getSubscribers(notification.getName(), doc); 395 for (String subscriptor : userGroup) { 396 if (subscriptor != null) { 397 if (isUser(subscriptor)) { 398 storeUserForNotification(notification, subscriptor.substring(5), targetUsers); 399 } else { 400 // it is a group - get all users and send 401 // notifications to them 402 List<String> usersOfGroup = getGroupMembers(subscriptor.substring(6)); 403 if (usersOfGroup != null && !usersOfGroup.isEmpty()) { 404 for (String usr : usersOfGroup) { 405 storeUserForNotification(notification, usr, targetUsers); 406 } 407 } 408 } 409 } 410 } 411 } else { 412 // An automatic notification happens 413 // should be sent to interested users 414 targetUsers.put(notification, new ArrayList<String>()); 415 } 416 } 417 } 418 419 private static void storeUserForNotification(Notification notification, String user, 420 Map<Notification, List<String>> targetUsers) { 421 List<String> subscribedUsers = targetUsers.get(notification); 422 if (subscribedUsers == null) { 423 targetUsers.put(notification, new ArrayList<String>()); 424 } 425 if (!targetUsers.get(notification).contains(user)) { 426 targetUsers.get(notification).add(user); 427 } 428 } 429 430 private boolean isDeleteEvent(String eventId) { 431 List<String> deletionEvents = new ArrayList<String>(); 432 deletionEvents.add("aboutToRemove"); 433 deletionEvents.add("documentRemoved"); 434 return deletionEvents.contains(eventId); 435 } 436 437 private boolean isUser(String subscriptor) { 438 return subscriptor != null && subscriptor.startsWith("user:"); 439 } 440 441 public boolean isInterestedInNotification(Notification notif) { 442 return notif != null && "email".equals(notif.getChannel()); 443 } 444 445 public DocumentViewCodecManager getDocLocator() { 446 if (docLocator == null) { 447 docLocator = Framework.getService(DocumentViewCodecManager.class); 448 } 449 return docLocator; 450 } 451 452 public EmailHelper getEmailHelper() { 453 return emailHelper; 454 } 455 456 public void setEmailHelper(EmailHelper emailHelper) { 457 this.emailHelper = emailHelper; 458 } 459 460}