001/* 002 * (C) Copyright 2007 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 * 019 * $Id$ 020 */ 021 022package org.nuxeo.ecm.platform.ec.notification.service; 023 024import java.io.Serializable; 025import java.net.URL; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collection; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033 034import javax.mail.MessagingException; 035 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.core.api.CoreSession; 039import org.nuxeo.ecm.core.api.DocumentLocation; 040import org.nuxeo.ecm.core.api.DocumentModel; 041import org.nuxeo.ecm.core.api.NuxeoException; 042import org.nuxeo.ecm.core.api.NuxeoPrincipal; 043import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 044import org.nuxeo.ecm.core.api.event.CoreEventConstants; 045import org.nuxeo.ecm.core.api.event.DocumentEventCategories; 046import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 047import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl; 048import org.nuxeo.ecm.core.event.Event; 049import org.nuxeo.ecm.core.event.EventProducer; 050import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 051import org.nuxeo.ecm.core.versioning.VersioningService; 052import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; 053import org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener; 054import org.nuxeo.ecm.platform.ec.notification.NotificationConstants; 055import org.nuxeo.ecm.platform.ec.notification.NotificationListenerHook; 056import org.nuxeo.ecm.platform.ec.notification.NotificationListenerVeto; 057import org.nuxeo.ecm.platform.ec.notification.SubscriptionAdapter; 058import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper; 059import org.nuxeo.ecm.platform.notification.api.Notification; 060import org.nuxeo.ecm.platform.notification.api.NotificationManager; 061import org.nuxeo.ecm.platform.notification.api.NotificationRegistry; 062import org.nuxeo.ecm.platform.url.DocumentViewImpl; 063import org.nuxeo.ecm.platform.url.api.DocumentView; 064import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager; 065import org.nuxeo.runtime.api.Framework; 066import org.nuxeo.runtime.model.ComponentContext; 067import org.nuxeo.runtime.model.ComponentName; 068import org.nuxeo.runtime.model.DefaultComponent; 069import org.nuxeo.runtime.model.Extension; 070 071/** 072 * @author <a href="mailto:npaslaru@nuxeo.com">Narcis Paslaru</a> 073 */ 074public class NotificationService extends DefaultComponent implements NotificationManager { 075 076 public static final ComponentName NAME = new ComponentName( 077 "org.nuxeo.ecm.platform.ec.notification.service.NotificationService"); 078 079 private static final Log log = LogFactory.getLog(NotificationService.class); 080 081 public static final String SUBSCRIPTION_NAME = "UserSubscription"; 082 083 protected static final String NOTIFICATIONS_EP = "notifications"; 084 085 protected static final String TEMPLATES_EP = "templates"; 086 087 protected static final String GENERAL_SETTINGS_EP = "generalSettings"; 088 089 protected static final String NOTIFICATION_HOOK_EP = "notificationListenerHook"; 090 091 protected static final String NOTIFICATION_VETO_EP = "notificationListenerVeto"; 092 093 // FIXME: performance issue when putting URLs in a Map. 094 protected static final Map<String, URL> TEMPLATES_MAP = new HashMap<String, URL>(); 095 096 protected EmailHelper emailHelper = new EmailHelper(); 097 098 protected GeneralSettingsDescriptor generalSettings; 099 100 protected NotificationRegistry notificationRegistry; 101 102 protected DocumentViewCodecManager docLocator; 103 104 protected final Map<String, NotificationListenerHook> hookListeners = new HashMap<String, NotificationListenerHook>(); 105 106 protected NotificationListenerVetoRegistry notificationVetoRegistry; 107 108 @Override 109 @SuppressWarnings("unchecked") 110 public <T> T getAdapter(Class<T> adapter) { 111 if (adapter.isAssignableFrom(NotificationManager.class)) { 112 return (T) this; 113 } 114 return null; 115 } 116 117 @Override 118 public void activate(ComponentContext context) { 119 notificationRegistry = new NotificationRegistryImpl(); 120 notificationVetoRegistry = new NotificationListenerVetoRegistry(); 121 122 // init default settings 123 generalSettings = new GeneralSettingsDescriptor(); 124 generalSettings.serverPrefix = "http://localhost:8080/nuxeo/"; 125 generalSettings.eMailSubjectPrefix = "[Nuxeo]"; 126 generalSettings.mailSessionJndiName = "java:/Mail"; 127 } 128 129 @Override 130 public void deactivate(ComponentContext context) { 131 notificationRegistry.clear(); 132 notificationVetoRegistry.clear(); 133 notificationRegistry = null; 134 notificationVetoRegistry = null; 135 } 136 137 @Override 138 public void registerExtension(Extension extension) { 139 log.info("Registering notification extension"); 140 String xp = extension.getExtensionPoint(); 141 if (NOTIFICATIONS_EP.equals(xp)) { 142 Object[] contribs = extension.getContributions(); 143 for (Object contrib : contribs) { 144 NotificationDescriptor notifDesc = (NotificationDescriptor) contrib; 145 notificationRegistry.registerNotification(notifDesc, getNames(notifDesc.getEvents())); 146 } 147 } else if (TEMPLATES_EP.equals(xp)) { 148 Object[] contribs = extension.getContributions(); 149 for (Object contrib : contribs) { 150 TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib; 151 templateDescriptor.setContext(extension.getContext()); 152 registerTemplate(templateDescriptor); 153 } 154 } else if (GENERAL_SETTINGS_EP.equals(xp)) { 155 Object[] contribs = extension.getContributions(); 156 for (Object contrib : contribs) { 157 registerGeneralSettings((GeneralSettingsDescriptor) contrib); 158 } 159 } else if (NOTIFICATION_HOOK_EP.equals(xp)) { 160 Object[] contribs = extension.getContributions(); 161 for (Object contrib : contribs) { 162 NotificationListenerHookDescriptor desc = (NotificationListenerHookDescriptor) contrib; 163 Class<? extends NotificationListenerHook> clazz = desc.hookListener; 164 try { 165 NotificationListenerHook hookListener = clazz.newInstance(); 166 registerHookListener(desc.name, hookListener); 167 } catch (ReflectiveOperationException e) { 168 log.error(e); 169 } 170 } 171 } else if (NOTIFICATION_VETO_EP.equals(xp)) { 172 Object[] contribs = extension.getContributions(); 173 for (Object contrib : contribs) { 174 NotificationListenerVetoDescriptor desc = (NotificationListenerVetoDescriptor) contrib; 175 notificationVetoRegistry.addContribution(desc); 176 } 177 } 178 } 179 180 private void registerHookListener(String name, NotificationListenerHook hookListener) { 181 hookListeners.put(name, hookListener); 182 } 183 184 protected void registerGeneralSettings(GeneralSettingsDescriptor desc) { 185 generalSettings = desc; 186 String serverPrefix = Framework.expandVars(generalSettings.serverPrefix); 187 if (serverPrefix != null) { 188 generalSettings.serverPrefix = serverPrefix.endsWith("//") ? serverPrefix.substring(0, 189 serverPrefix.length() - 1) : serverPrefix; 190 } 191 generalSettings.eMailSubjectPrefix = Framework.expandVars(generalSettings.eMailSubjectPrefix); 192 generalSettings.mailSessionJndiName = Framework.expandVars(generalSettings.mailSessionJndiName); 193 } 194 195 private static List<String> getNames(List<NotificationEventDescriptor> events) { 196 List<String> eventNames = new ArrayList<String>(); 197 for (NotificationEventDescriptor descriptor : events) { 198 eventNames.add(descriptor.name); 199 } 200 return eventNames; 201 } 202 203 @Override 204 public void unregisterExtension(Extension extension) { 205 String xp = extension.getExtensionPoint(); 206 if (NOTIFICATIONS_EP.equals(xp)) { 207 Object[] contribs = extension.getContributions(); 208 for (Object contrib : contribs) { 209 NotificationDescriptor notifDesc = (NotificationDescriptor) contrib; 210 notificationRegistry.unregisterNotification(notifDesc, getNames(notifDesc.getEvents())); 211 } 212 } else if (TEMPLATES_EP.equals(xp)) { 213 Object[] contribs = extension.getContributions(); 214 for (Object contrib : contribs) { 215 TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib; 216 templateDescriptor.setContext(extension.getContext()); 217 unregisterTemplate(templateDescriptor); 218 } 219 } else if (NOTIFICATION_VETO_EP.equals(xp)) { 220 Object[] contribs = extension.getContributions(); 221 for (Object contrib : contribs) { 222 NotificationListenerVetoDescriptor vetoDescriptor = (NotificationListenerVetoDescriptor) contrib; 223 notificationVetoRegistry.removeContribution(vetoDescriptor); 224 } 225 } 226 } 227 228 @Override 229 public NotificationRegistry getNotificationRegistry() { 230 return notificationRegistry; 231 } 232 233 public NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() { 234 return notificationVetoRegistry; 235 } 236 237 /** 238 * @deprecated since 7.3 239 * @see NotificationService#getSubscribers(String, DocumentModel) 240 */ 241 @Deprecated 242 @Override 243 public List<String> getSubscribers(String notification, String docId) { 244 return getSubscribers(notification, UnrestrictedDocFetcher.fetch(docId)); 245 } 246 247 @Override 248 public List<String> getSubscribers(String notification, DocumentModel doc) { 249 return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification); 250 } 251 252 /** 253 * @deprecated since 7.3 254 * @see NotificationService#getSubscriptionsForUserOnDocument(String, DocumentModel) 255 */ 256 257 @Deprecated 258 @Override 259 public List<String> getSubscriptionsForUserOnDocument(String username, String docId) { 260 return getSubscriptionsForUserOnDocument(username, UnrestrictedDocFetcher.fetch(docId)); 261 } 262 263 @Override 264 public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) { 265 return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username); 266 } 267 268 private void disableEvents(DocumentModel doc) { 269 doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true); 270 doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true); 271 doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true); 272 doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true); 273 274 } 275 276 @Override 277 public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail, 278 NuxeoPrincipal principal, String notificationName) { 279 280 UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) { 281 282 @Override 283 public void run() { 284 doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification); 285 disableEvents(doc); 286 session.saveDocument(doc); 287 } 288 289 }; 290 291 runner.runUnrestricted(); 292 293 // send event for email if necessary 294 if (sendConfirmationEmail) { 295 raiseConfirmationEvent(principal, doc, username, notificationName); 296 } 297 } 298 299 @Override 300 public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail, 301 NuxeoPrincipal principal) { 302 UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) { 303 304 @Override 305 public void run() { 306 doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username); 307 disableEvents(doc); 308 session.saveDocument(doc); 309 } 310 }; 311 runner.runUnrestricted(); 312 313 // send event for email if necessary 314 if (sendConfirmationEmail) { 315 raiseConfirmationEvent(principal, doc, username, "All Notifications"); 316 } 317 } 318 319 @Override 320 public void removeSubscriptions(String username, List<String> notifications, String docId) { 321 removeSubscriptions(username, notifications, UnrestrictedDocFetcher.fetch(docId)); 322 } 323 324 @Override 325 public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc) { 326 UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) { 327 328 @Override 329 public void run() { 330 SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class); 331 for (String notification : notifications) { 332 sa.removeUserNotificationSubscription(username, notification); 333 } 334 disableEvents(doc); 335 session.saveDocument(doc); 336 } 337 }; 338 runner.runUnrestricted(); 339 340 } 341 342 protected EventProducer producer; 343 344 protected void doFireEvent(Event event) { 345 if (producer == null) { 346 producer = Framework.getService(EventProducer.class); 347 } 348 producer.fireEvent(event); 349 } 350 351 private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username, 352 String notification) { 353 354 Map<String, Serializable> options = new HashMap<String, Serializable>(); 355 356 // Name of the current repository 357 options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName()); 358 359 // Add the session ID 360 options.put(CoreEventConstants.SESSION_ID, doc.getSessionId()); 361 362 // options for confirmation email 363 options.put("recipients", username); 364 options.put("notifName", notification); 365 366 CoreSession session = doc.getCoreSession(); 367 DocumentEventContext ctx = new DocumentEventContext(session, principal, doc); 368 ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY); 369 ctx.setProperties(options); 370 Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED); 371 doFireEvent(event); 372 } 373 374 @Override 375 public void removeSubscription(String username, String notification, String docId) { 376 removeSubscription(username, notification, UnrestrictedDocFetcher.fetch(docId)); 377 } 378 379 @Override 380 public void removeSubscription(String username, String notification, DocumentModel doc) { 381 removeSubscriptions(username, Arrays.asList(new String[] { notification }), doc); 382 } 383 384 /** 385 * @param notification 386 * @param docId 387 * @return 388 * @deprecated 389 * @see NotificationService#getSubscribers(String, DocumentModel) 390 */ 391 @Deprecated 392 @Override 393 public List<String> getUsersSubscribedToNotificationOnDocument(String notification, String docId) { 394 return getSubscribers(notification, docId); 395 } 396 397 private static void registerTemplate(TemplateDescriptor td) { 398 if (td.src != null && td.src.length() > 0) { 399 URL url = td.getContext().getResource(td.src); 400 TEMPLATES_MAP.put(td.name, url); 401 } 402 } 403 404 private static void unregisterTemplate(TemplateDescriptor td) { 405 if (td.name != null) { 406 TEMPLATES_MAP.remove(td.name); 407 } 408 } 409 410 public static URL getTemplateURL(String name) { 411 return TEMPLATES_MAP.get(name); 412 } 413 414 public String getServerUrlPrefix() { 415 return generalSettings.getServerPrefix(); 416 } 417 418 public String getEMailSubjectPrefix() { 419 return generalSettings.getEMailSubjectPrefix(); 420 } 421 422 public String getMailSessionJndiName() { 423 return generalSettings.getMailSessionJndiName(); 424 } 425 426 @Override 427 public Notification getNotificationByName(String selectedNotification) { 428 List<Notification> listNotif = notificationRegistry.getNotifications(); 429 for (Notification notification : listNotif) { 430 if (notification.getName().equals(selectedNotification)) { 431 return notification; 432 } 433 } 434 return null; 435 } 436 437 @Override 438 public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal) { 439 440 Notification notif = getNotificationByName(notificationName); 441 442 NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal); 443 String email = recipient.getEmail(); 444 String mailTemplate = notif.getTemplate(); 445 446 infoMap.put("mail.to", email); 447 448 String authorUsername = (String) infoMap.get("author"); 449 450 if (authorUsername != null) { 451 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 452 infoMap.put("principalAuthor", author); 453 } 454 455 // mail.put("doc", docMessage); - should be already there 456 457 String subject = notif.getSubject() == null ? "Alert" : notif.getSubject(); 458 if (notif.getSubjectTemplate() != null) { 459 subject = notif.getSubjectTemplate(); 460 } 461 462 subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject; 463 464 infoMap.put("subject", subject); 465 infoMap.put("template", mailTemplate); 466 467 try { 468 emailHelper.sendmail(infoMap); 469 } catch (MessagingException e) { 470 throw new NuxeoException("Failed to send notification email ", e); 471 } 472 } 473 474 @Override 475 public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment, 476 NuxeoPrincipal sender, List<String> sendTo) { 477 Map<String, Object> infoMap = new HashMap<String, Object>(); 478 infoMap.put("document", doc); 479 infoMap.put("subject", subject); 480 infoMap.put("comment", comment); 481 infoMap.put("sender", sender); 482 483 DocumentLocation docLoc = new DocumentLocationImpl(doc); 484 DocumentView docView = new DocumentViewImpl(docLoc); 485 docView.setViewId("view_documents"); 486 infoMap.put( 487 "docUrl", 488 getDocLocator().getUrlFromDocumentView(docView, true, 489 NotificationServiceHelper.getNotificationService().getServerUrlPrefix())); 490 491 if (freemarkerTemplateName == null) { 492 freemarkerTemplateName = "defaultNotifTemplate"; 493 } 494 infoMap.put("template", freemarkerTemplateName); 495 496 for (String to : sendTo) { 497 infoMap.put("mail.to", to); 498 try { 499 emailHelper.sendmail(infoMap); 500 } catch (MessagingException e) { 501 log.debug("Failed to send notification email " + e); 502 } 503 } 504 } 505 506 private DocumentViewCodecManager getDocLocator() { 507 if (docLocator == null) { 508 docLocator = Framework.getService(DocumentViewCodecManager.class); 509 } 510 return docLocator; 511 } 512 513 @Override 514 public List<Notification> getNotificationsForSubscriptions(String parentType) { 515 return notificationRegistry.getNotificationsForSubscriptions(parentType); 516 } 517 518 @Override 519 public List<Notification> getNotificationsForEvents(String eventId) { 520 return notificationRegistry.getNotificationsForEvent(eventId); 521 } 522 523 public EmailHelper getEmailHelper() { 524 return emailHelper; 525 } 526 527 public void setEmailHelper(EmailHelper emailHelper) { 528 this.emailHelper = emailHelper; 529 } 530 531 @Override 532 public Set<String> getNotificationEventNames() { 533 return notificationRegistry.getNotificationEventNames(); 534 } 535 536 public Collection<NotificationListenerHook> getListenerHooks() { 537 return hookListeners.values(); 538 } 539 540 public Collection<NotificationListenerVeto> getNotificationVetos() { 541 return notificationVetoRegistry.getVetos(); 542 } 543 544 @Override 545 public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc) { 546 return getSubscribers(notification, doc); 547 } 548 549 @Override 550 public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName) { 551 String nxql = String.format("SELECT * FROM Document WHERE ecm:mixinType ='Notifiable' " 552 + "AND ecm:isCheckedInVersion = 0 " 553 + "AND notif:notifications/*/subscribers/* = '%s'", prefixedPrincipalName); 554 555 return UnrestrictedDocFetcher.query(nxql); 556 } 557 558}