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.event.Event;
049import org.nuxeo.ecm.core.event.EventProducer;
050import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
051import org.nuxeo.ecm.core.query.sql.NXQL;
052import org.nuxeo.ecm.core.versioning.VersioningService;
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.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}