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}