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