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