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}