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