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