001/*
002 * (C) Copyright 2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.platform.ec.notification.service;
021
022import java.io.Serializable;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import javax.mail.MessagingException;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.DataModel;
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.UnrestrictedSessionRunner;
043import org.nuxeo.ecm.core.api.event.CoreEventConstants;
044import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
045import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
046import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
047import org.nuxeo.ecm.core.event.Event;
048import org.nuxeo.ecm.core.event.EventProducer;
049import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
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<String, URL>();
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<String, NotificationListenerHook>();
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 = (NotificationListenerHook) 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("//") ? serverPrefix.substring(0,
188                    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<String>();
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 NotificationRegistry getNotificationRegistry() {
228        return notificationRegistry;
229    }
230
231    public NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() {
232        return notificationVetoRegistry;
233    }
234
235    /**
236     * @deprecated since 7.3
237     * @see NotificationService#getSubscribers(String, DocumentModel)
238     */
239    public List<String> getSubscribers(String notification, String docId) {
240        return getSubscribers(notification, UnrestrictedDocFetcher.fetch(docId));
241    }
242
243    @Override
244    public List<String> getSubscribers(String notification, DocumentModel doc) {
245        return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification);
246    }
247
248    /**
249     * @deprecated since 7.3
250     * @see NotificationService#getSubscriptionsForUserOnDocument(String, DocumentModel)
251     */
252
253    public List<String> getSubscriptionsForUserOnDocument(String username, String docId) {
254        return getSubscriptionsForUserOnDocument(username, UnrestrictedDocFetcher.fetch(docId));
255    }
256
257    @Override
258    public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) {
259        return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username);
260    }
261
262    private void disableEvents(DocumentModel doc) {
263        doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true);
264        doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true);
265        doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true);
266        doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true);
267
268    }
269
270    public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail,
271            NuxeoPrincipal principal, String notificationName) {
272
273        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
274
275            @Override
276            public void run() {
277                doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification);
278                disableEvents(doc);
279                session.saveDocument(doc);
280            }
281
282        };
283
284        runner.runUnrestricted();
285
286        // send event for email if necessary
287        if (sendConfirmationEmail) {
288            raiseConfirmationEvent(principal, doc, username, notificationName);
289        }
290    }
291
292    public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail,
293            NuxeoPrincipal principal) {
294        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
295
296            @Override
297            public void run() {
298                doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username);
299                disableEvents(doc);
300                session.saveDocument(doc);
301            }
302        };
303        runner.runUnrestricted();
304
305        // send event for email if necessary
306        if (sendConfirmationEmail) {
307            raiseConfirmationEvent(principal, doc, username, "All Notifications");
308        }
309    }
310
311    public void removeSubscriptions(String username, List<String> notifications, String docId) {
312        removeSubscriptions(username, notifications, UnrestrictedDocFetcher.fetch(docId));
313    }
314
315    public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc)
316            {
317        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
318
319            @Override
320            public void run() {
321                SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class);
322                for (String notification : notifications) {
323                    sa.removeUserNotificationSubscription(username, notification);
324                }
325                disableEvents(doc);
326                session.saveDocument(doc);
327            }
328        };
329        runner.runUnrestricted();
330
331    }
332
333    protected EventProducer producer;
334
335    protected void doFireEvent(Event event) {
336        if (producer == null) {
337            producer = Framework.getService(EventProducer.class);
338        }
339        producer.fireEvent(event);
340    }
341
342    private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username,
343            String notification) {
344
345        Map<String, Serializable> options = new HashMap<String, Serializable>();
346
347        // Name of the current repository
348        options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName());
349
350        // Add the session ID
351        options.put(CoreEventConstants.SESSION_ID, doc.getSessionId());
352
353        // options for confirmation email
354        options.put("recipients", username);
355        options.put("notifName", notification);
356
357        CoreSession session = doc.getCoreSession();
358        DocumentEventContext ctx = new DocumentEventContext(session, principal, doc);
359        ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY);
360        ctx.setProperties(options);
361        Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED);
362        doFireEvent(event);
363    }
364
365    @Override
366    public void removeSubscription(String username, String notification, String docId) {
367        removeSubscription(username, notification, UnrestrictedDocFetcher.fetch(docId));
368    }
369
370    @Override
371    public void removeSubscription(String username, String notification, DocumentModel doc) {
372        removeSubscriptions(username, Arrays.asList(new String[] { notification }), doc);
373    }
374
375    /**
376     * @param notification
377     * @param docId
378     * @return
379     * @deprecated
380     * @see NotificationService#getSubscribers(String, DocumentModel)
381     */
382    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, String docId)
383            {
384        return getSubscribers(notification, docId);
385    }
386
387    private static void registerTemplate(TemplateDescriptor td) {
388        if (td.src != null && td.src.length() > 0) {
389            URL url = td.getContext().getResource(td.src);
390            TEMPLATES_MAP.put(td.name, url);
391        }
392    }
393
394    private static void unregisterTemplate(TemplateDescriptor td) {
395        if (td.name != null) {
396            TEMPLATES_MAP.remove(td.name);
397        }
398    }
399
400    public static URL getTemplateURL(String name) {
401        return TEMPLATES_MAP.get(name);
402    }
403
404    public String getServerUrlPrefix() {
405        return generalSettings.getServerPrefix();
406    }
407
408    public String getEMailSubjectPrefix() {
409        return generalSettings.getEMailSubjectPrefix();
410    }
411
412    public String getMailSessionJndiName() {
413        return generalSettings.getMailSessionJndiName();
414    }
415
416    public Notification getNotificationByName(String selectedNotification) {
417        List<Notification> listNotif = notificationRegistry.getNotifications();
418        for (Notification notification : listNotif) {
419            if (notification.getName().equals(selectedNotification)) {
420                return notification;
421            }
422        }
423        return null;
424    }
425
426    public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal)
427            {
428
429        Notification notif = getNotificationByName(notificationName);
430
431        NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal);
432        // XXX hack, principals have only one model
433        DataModel model = recipient.getModel().getDataModels().values().iterator().next();
434        String email = (String) model.getData("email");
435        String mailTemplate = notif.getTemplate();
436
437        infoMap.put("mail.to", email);
438
439        String authorUsername = (String) infoMap.get("author");
440
441        if (authorUsername != null) {
442            NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
443            infoMap.put("principalAuthor", author);
444        }
445
446        // mail.put("doc", docMessage); - should be already there
447
448        String subject = notif.getSubject() == null ? "Alert" : notif.getSubject();
449        if (notif.getSubjectTemplate() != null) {
450            subject = notif.getSubjectTemplate();
451        }
452
453        subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject;
454
455        infoMap.put("subject", subject);
456        infoMap.put("template", mailTemplate);
457
458        try {
459            emailHelper.sendmail(infoMap);
460        } catch (MessagingException e) {
461            throw new NuxeoException("Failed to send notification email ", e);
462        }
463    }
464
465    public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment,
466            NuxeoPrincipal sender, List<String> sendTo) {
467        Map<String, Object> infoMap = new HashMap<String, Object>();
468        infoMap.put("document", doc);
469        infoMap.put("subject", subject);
470        infoMap.put("comment", comment);
471        infoMap.put("sender", sender);
472
473        DocumentLocation docLoc = new DocumentLocationImpl(doc);
474        DocumentView docView = new DocumentViewImpl(docLoc);
475        docView.setViewId("view_documents");
476        infoMap.put(
477                "docUrl",
478                getDocLocator().getUrlFromDocumentView(docView, true,
479                        NotificationServiceHelper.getNotificationService().getServerUrlPrefix()));
480
481        if (freemarkerTemplateName == null) {
482            freemarkerTemplateName = "defaultNotifTemplate";
483        }
484        infoMap.put("template", freemarkerTemplateName);
485
486        for (String to : sendTo) {
487            infoMap.put("mail.to", to);
488            try {
489                emailHelper.sendmail(infoMap);
490            } catch (MessagingException e) {
491                log.debug("Failed to send notification email " + e);
492            }
493        }
494    }
495
496    private DocumentViewCodecManager getDocLocator() {
497        if (docLocator == null) {
498            docLocator = Framework.getService(DocumentViewCodecManager.class);
499        }
500        return docLocator;
501    }
502
503    public List<Notification> getNotificationsForSubscriptions(String parentType) {
504        return notificationRegistry.getNotificationsForSubscriptions(parentType);
505    }
506
507    public List<Notification> getNotificationsForEvents(String eventId) {
508        return notificationRegistry.getNotificationsForEvent(eventId);
509    }
510
511    public EmailHelper getEmailHelper() {
512        return emailHelper;
513    }
514
515    public void setEmailHelper(EmailHelper emailHelper) {
516        this.emailHelper = emailHelper;
517    }
518
519    @Override
520    public Set<String> getNotificationEventNames() {
521        return notificationRegistry.getNotificationEventNames();
522    }
523
524    public Collection<NotificationListenerHook> getListenerHooks() {
525        return hookListeners.values();
526    }
527
528    public Collection<NotificationListenerVeto> getNotificationVetos() {
529        return notificationVetoRegistry.getVetos();
530    }
531
532    @Override
533    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc)
534            {
535        return getSubscribers(notification, doc);
536    }
537
538    @Override
539    public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName) {
540        String nxql = String.format("SELECT * FROM Document WHERE ecm:mixinType ='Notifiable' "
541                + "AND notif:notifications/*/subscribers/* = '%s'", prefixedPrincipalName);
542
543        return UnrestrictedDocFetcher.query(nxql);
544    }
545
546}