001/* 002 * (C) Copyright 2007-2016 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 */ 019package org.nuxeo.ecm.platform.ec.notification.service; 020 021import java.io.Serializable; 022import java.net.URL; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.stream.Collectors; 031 032import javax.mail.MessagingException; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.ecm.core.api.CoreInstance; 037import org.nuxeo.ecm.core.api.CoreSession; 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.event.CoreEventConstants; 043import org.nuxeo.ecm.core.api.event.DocumentEventCategories; 044import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 045import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl; 046import org.nuxeo.ecm.core.event.Event; 047import org.nuxeo.ecm.core.event.EventProducer; 048import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 049import org.nuxeo.ecm.core.query.sql.NXQL; 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<>(); 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<>(); 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 = 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("//") 188 ? serverPrefix.substring(0, 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<>(); 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 NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() { 228 return notificationVetoRegistry; 229 } 230 231 @Override 232 public List<String> getSubscribers(String notification, DocumentModel doc) { 233 return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification); 234 } 235 236 @Override 237 public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) { 238 return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username); 239 } 240 241 protected void disableEvents(DocumentModel doc) { 242 doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true); 243 doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true); 244 doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true); 245 doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true); 246 } 247 248 protected void restoreEvents(DocumentModel doc) { 249 doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, null); 250 doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, null); 251 doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, null); 252 doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, null); 253 } 254 255 @Override 256 public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail, 257 NuxeoPrincipal principal, String notificationName) { 258 259 CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> { 260 doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification); 261 disableEvents(doc); 262 session.saveDocument(doc); 263 restoreEvents(doc); 264 }); 265 266 // send event for email if necessary 267 if (sendConfirmationEmail) { 268 raiseConfirmationEvent(principal, doc, username, notificationName); 269 } 270 } 271 272 @Override 273 public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail, 274 NuxeoPrincipal principal) { 275 CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> { 276 doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username); 277 disableEvents(doc); 278 session.saveDocument(doc); 279 restoreEvents(doc); 280 }); 281 282 // send event for email if necessary 283 if (sendConfirmationEmail) { 284 raiseConfirmationEvent(principal, doc, username, "All Notifications"); 285 } 286 } 287 288 @Override 289 public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc) { 290 CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> { 291 SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class); 292 for (String notification : notifications) { 293 sa.removeUserNotificationSubscription(username, notification); 294 } 295 disableEvents(doc); 296 session.saveDocument(doc); 297 restoreEvents(doc); 298 }); 299 } 300 301 protected EventProducer producer; 302 303 protected void doFireEvent(Event event) { 304 if (producer == null) { 305 producer = Framework.getService(EventProducer.class); 306 } 307 producer.fireEvent(event); 308 } 309 310 private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username, 311 String notification) { 312 313 Map<String, Serializable> options = new HashMap<>(); 314 315 // Name of the current repository 316 options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName()); 317 318 // Add the session ID 319 options.put(CoreEventConstants.SESSION_ID, doc.getSessionId()); 320 321 // options for confirmation email 322 options.put("recipients", username); 323 options.put("notifName", notification); 324 325 CoreSession session = doc.getCoreSession(); 326 DocumentEventContext ctx = new DocumentEventContext(session, principal, doc); 327 ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY); 328 ctx.setProperties(options); 329 Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED); 330 doFireEvent(event); 331 } 332 333 @Override 334 public void removeSubscription(String username, String notification, DocumentModel doc) { 335 removeSubscriptions(username, Arrays.asList(notification), doc); 336 } 337 338 private static void registerTemplate(TemplateDescriptor td) { 339 if (td.src != null && td.src.length() > 0) { 340 URL url = td.getContext().getResource(td.src); 341 TEMPLATES_MAP.put(td.name, url); 342 } 343 } 344 345 private static void unregisterTemplate(TemplateDescriptor td) { 346 if (td.name != null) { 347 TEMPLATES_MAP.remove(td.name); 348 } 349 } 350 351 public static URL getTemplateURL(String name) { 352 return TEMPLATES_MAP.get(name); 353 } 354 355 public String getServerUrlPrefix() { 356 return generalSettings.getServerPrefix(); 357 } 358 359 public String getEMailSubjectPrefix() { 360 return generalSettings.getEMailSubjectPrefix(); 361 } 362 363 public String getMailSessionJndiName() { 364 return generalSettings.getMailSessionJndiName(); 365 } 366 367 @Override 368 public Notification getNotificationByName(String selectedNotification) { 369 List<Notification> listNotif = notificationRegistry.getNotifications(); 370 for (Notification notification : listNotif) { 371 if (notification.getName().equals(selectedNotification)) { 372 return notification; 373 } 374 } 375 return null; 376 } 377 378 @Override 379 public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal) { 380 381 Notification notif = getNotificationByName(notificationName); 382 383 NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal); 384 String email = recipient.getEmail(); 385 String mailTemplate = notif.getTemplate(); 386 387 infoMap.put("mail.to", email); 388 389 String authorUsername = (String) infoMap.get("author"); 390 391 if (authorUsername != null) { 392 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 393 infoMap.put("principalAuthor", author); 394 } 395 396 // mail.put("doc", docMessage); - should be already there 397 398 String subject = notif.getSubject() == null ? "Alert" : notif.getSubject(); 399 if (notif.getSubjectTemplate() != null) { 400 subject = notif.getSubjectTemplate(); 401 } 402 403 subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject; 404 405 infoMap.put("subject", subject); 406 infoMap.put("template", mailTemplate); 407 408 try { 409 emailHelper.sendmail(infoMap); 410 } catch (MessagingException e) { 411 throw new NuxeoException("Failed to send notification email ", e); 412 } 413 } 414 415 @Override 416 public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment, 417 NuxeoPrincipal sender, List<String> sendTo) { 418 Map<String, Object> infoMap = new HashMap<>(); 419 infoMap.put("document", doc); 420 infoMap.put("subject", subject); 421 infoMap.put("comment", comment); 422 infoMap.put("sender", sender); 423 424 DocumentLocation docLoc = new DocumentLocationImpl(doc); 425 DocumentView docView = new DocumentViewImpl(docLoc); 426 docView.setViewId("view_documents"); 427 infoMap.put("docUrl", getDocLocator().getUrlFromDocumentView(docView, true, 428 NotificationServiceHelper.getNotificationService().getServerUrlPrefix())); 429 430 if (freemarkerTemplateName == null) { 431 freemarkerTemplateName = "defaultNotifTemplate"; 432 } 433 infoMap.put("template", freemarkerTemplateName); 434 435 for (String to : sendTo) { 436 infoMap.put("mail.to", to); 437 try { 438 emailHelper.sendmail(infoMap); 439 } catch (MessagingException e) { 440 log.debug("Failed to send notification email " + e); 441 } 442 } 443 } 444 445 private DocumentViewCodecManager getDocLocator() { 446 if (docLocator == null) { 447 docLocator = Framework.getService(DocumentViewCodecManager.class); 448 } 449 return docLocator; 450 } 451 452 @Override 453 public List<Notification> getNotificationsForSubscriptions(String parentType) { 454 return notificationRegistry.getNotificationsForSubscriptions(parentType); 455 } 456 457 @Override 458 public List<Notification> getNotificationsForEvents(String eventId) { 459 return notificationRegistry.getNotificationsForEvent(eventId); 460 } 461 462 public EmailHelper getEmailHelper() { 463 return emailHelper; 464 } 465 466 public void setEmailHelper(EmailHelper emailHelper) { 467 this.emailHelper = emailHelper; 468 } 469 470 @Override 471 public Set<String> getNotificationEventNames() { 472 return notificationRegistry.getNotificationEventNames(); 473 } 474 475 public Collection<NotificationListenerHook> getListenerHooks() { 476 return hookListeners.values(); 477 } 478 479 public Collection<NotificationListenerVeto> getNotificationVetos() { 480 return notificationVetoRegistry.getVetos(); 481 } 482 483 @Override 484 public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc) { 485 return getSubscribers(notification, doc); 486 } 487 488 @Override 489 public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName, String repositoryName) { 490 String nxql = "SELECT * FROM Document WHERE ecm:mixinType = '" + SubscriptionAdapter.NOTIFIABLE_FACET + "' " 491 + "AND ecm:isCheckedInVersion = 0 " + "AND notif:notifications/*/subscribers/* = " 492 + NXQL.escapeString(prefixedPrincipalName); 493 494 return CoreInstance.doPrivileged(repositoryName, 495 (CoreSession s) -> s.query(nxql).stream().map(NotificationService::detachDocumentModel).collect( 496 Collectors.toList())); 497 } 498 499 protected static DocumentModel detachDocumentModel(DocumentModel doc) { 500 doc.detach(true); 501 return doc; 502 } 503 504}