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