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