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:[email protected]">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.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 // Add the session ID 324 options.put(CoreEventConstants.SESSION_ID, doc.getSessionId()); 325 326 // options for confirmation email 327 options.put("recipients", username); 328 options.put("notifName", notification); 329 330 CoreSession session = doc.getCoreSession(); 331 DocumentEventContext ctx = new DocumentEventContext(session, principal, doc); 332 ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY); 333 ctx.setProperties(options); 334 Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED); 335 doFireEvent(event); 336 } 337 338 @Override 339 public void removeSubscription(String username, String notification, DocumentModel doc) { 340 removeSubscriptions(username, singletonList(notification), doc); 341 } 342 343 private static void registerTemplate(TemplateDescriptor td) { 344 if (td.src != null && td.src.length() > 0) { 345 URL url = td.getContext().getResource(td.src); 346 TEMPLATES_MAP.put(td.name, url); 347 } 348 } 349 350 private static void unregisterTemplate(TemplateDescriptor td) { 351 if (td.name != null) { 352 TEMPLATES_MAP.remove(td.name); 353 } 354 } 355 356 public static URL getTemplateURL(String name) { 357 return TEMPLATES_MAP.get(name); 358 } 359 360 public String getServerUrlPrefix() { 361 return generalSettings.getServerPrefix(); 362 } 363 364 public String getEMailSubjectPrefix() { 365 return generalSettings.getEMailSubjectPrefix(); 366 } 367 368 public String getMailSessionJndiName() { 369 return generalSettings.getMailSessionJndiName(); 370 } 371 372 @Override 373 public Notification getNotificationByName(String selectedNotification) { 374 List<Notification> listNotif = notificationRegistry.getNotifications(); 375 for (Notification notification : listNotif) { 376 if (notification.getName().equals(selectedNotification)) { 377 return notification; 378 } 379 } 380 return null; 381 } 382 383 @Override 384 public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal) { 385 386 Notification notif = getNotificationByName(notificationName); 387 388 NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal); 389 String email = recipient.getEmail(); 390 String mailTemplate = notif.getTemplate(); 391 392 infoMap.put("mail.to", email); 393 394 String authorUsername = (String) infoMap.get("author"); 395 396 if (authorUsername != null) { 397 NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername); 398 infoMap.put("principalAuthor", author); 399 } 400 401 // mail.put("doc", docMessage); - should be already there 402 403 String subject = notif.getSubject() == null ? "Alert" : notif.getSubject(); 404 if (notif.getSubjectTemplate() != null) { 405 subject = notif.getSubjectTemplate(); 406 } 407 408 subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject; 409 410 infoMap.put("subject", subject); 411 infoMap.put("template", mailTemplate); 412 413 try { 414 emailHelper.sendmail(infoMap); 415 } catch (MessagingException e) { 416 throw new NuxeoException("Failed to send notification email ", e); 417 } 418 } 419 420 @Override 421 public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment, 422 NuxeoPrincipal sender, List<String> sendTo) { 423 Map<String, Object> infoMap = new HashMap<>(); 424 infoMap.put("document", doc); 425 infoMap.put("subject", subject); 426 infoMap.put("comment", comment); 427 infoMap.put("sender", sender); 428 429 DocumentLocation docLoc = new DocumentLocationImpl(doc); 430 DocumentView docView = new DocumentViewImpl(docLoc); 431 docView.setViewId("view_documents"); 432 infoMap.put("docUrl", getDocLocator().getUrlFromDocumentView(docView, true, 433 NotificationServiceHelper.getNotificationService().getServerUrlPrefix())); 434 435 if (freemarkerTemplateName == null) { 436 freemarkerTemplateName = "defaultNotifTemplate"; 437 } 438 infoMap.put("template", freemarkerTemplateName); 439 440 for (String to : sendTo) { 441 infoMap.put("mail.to", to); 442 try { 443 emailHelper.sendmail(infoMap); 444 } catch (MessagingException e) { 445 log.debug("Failed to send notification email " + e); 446 } 447 } 448 } 449 450 private DocumentViewCodecManager getDocLocator() { 451 if (docLocator == null) { 452 docLocator = Framework.getService(DocumentViewCodecManager.class); 453 } 454 return docLocator; 455 } 456 457 @Override 458 public List<Notification> getNotificationsForSubscriptions(String parentType) { 459 return notificationRegistry.getNotificationsForSubscriptions(parentType); 460 } 461 462 @Override 463 public List<Notification> getNotificationsForEvents(String eventId) { 464 return notificationRegistry.getNotificationsForEvent(eventId); 465 } 466 467 public EmailHelper getEmailHelper() { 468 return emailHelper; 469 } 470 471 public void setEmailHelper(EmailHelper emailHelper) { 472 this.emailHelper = emailHelper; 473 } 474 475 @Override 476 public Set<String> getNotificationEventNames() { 477 return notificationRegistry.getNotificationEventNames(); 478 } 479 480 public Collection<NotificationListenerHook> getListenerHooks() { 481 return hookListeners.values(); 482 } 483 484 public Collection<NotificationListenerVeto> getNotificationVetos() { 485 return notificationVetoRegistry.getVetos(); 486 } 487 488 @Override 489 public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc) { 490 return getSubscribers(notification, doc); 491 } 492 493 @Override 494 public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName, String repositoryName) { 495 String nxql = "SELECT * FROM Document WHERE ecm:mixinType = '" + SubscriptionAdapter.NOTIFIABLE_FACET + "' " 496 + "AND ecm:isVersion = 0 " + "AND notif:notifications/*/subscribers/* = " 497 + NXQL.escapeString(prefixedPrincipalName); 498 499 return CoreInstance.doPrivileged(repositoryName, 500 (CoreSession s) -> s.query(nxql).stream().map(NotificationService::detachDocumentModel).collect( 501 toList())); 502 } 503 504 protected static DocumentModel detachDocumentModel(DocumentModel doc) { 505 doc.detach(true); 506 return doc; 507 } 508 509}