001/*
002 * (C) Copyright 2007 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 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.platform.ec.notification.service;
023
024import java.io.Serializable;
025import java.net.URL;
026import java.util.ArrayList;
027import java.util.Arrays;
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.CoreSession;
039import org.nuxeo.ecm.core.api.DataModel;
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.UnrestrictedSessionRunner;
045import org.nuxeo.ecm.core.api.event.CoreEventConstants;
046import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
047import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
048import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
049import org.nuxeo.ecm.core.event.Event;
050import org.nuxeo.ecm.core.event.EventProducer;
051import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
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    public static final String SUBSCRIPTION_NAME = "UserSubscription";
083
084    protected static final String NOTIFICATIONS_EP = "notifications";
085
086    protected static final String TEMPLATES_EP = "templates";
087
088    protected static final String GENERAL_SETTINGS_EP = "generalSettings";
089
090    protected static final String NOTIFICATION_HOOK_EP = "notificationListenerHook";
091
092    protected static final String NOTIFICATION_VETO_EP = "notificationListenerVeto";
093
094    // FIXME: performance issue when putting URLs in a Map.
095    protected static final Map<String, URL> TEMPLATES_MAP = new HashMap<String, URL>();
096
097    protected EmailHelper emailHelper = new EmailHelper();
098
099    protected GeneralSettingsDescriptor generalSettings;
100
101    protected NotificationRegistry notificationRegistry;
102
103    protected DocumentViewCodecManager docLocator;
104
105    protected final Map<String, NotificationListenerHook> hookListeners = new HashMap<String, NotificationListenerHook>();
106
107    protected NotificationListenerVetoRegistry notificationVetoRegistry;
108
109    @Override
110    @SuppressWarnings("unchecked")
111    public <T> T getAdapter(Class<T> adapter) {
112        if (adapter.isAssignableFrom(NotificationManager.class)) {
113            return (T) this;
114        }
115        return null;
116    }
117
118    @Override
119    public void activate(ComponentContext context) {
120        notificationRegistry = new NotificationRegistryImpl();
121        notificationVetoRegistry = new NotificationListenerVetoRegistry();
122
123        // init default settings
124        generalSettings = new GeneralSettingsDescriptor();
125        generalSettings.serverPrefix = "http://localhost:8080/nuxeo/";
126        generalSettings.eMailSubjectPrefix = "[Nuxeo]";
127        generalSettings.mailSessionJndiName = "java:/Mail";
128    }
129
130    @Override
131    public void deactivate(ComponentContext context) {
132        notificationRegistry.clear();
133        notificationVetoRegistry.clear();
134        notificationRegistry = null;
135        notificationVetoRegistry = null;
136    }
137
138    @Override
139    public void registerExtension(Extension extension) {
140        log.info("Registering notification extension");
141        String xp = extension.getExtensionPoint();
142        if (NOTIFICATIONS_EP.equals(xp)) {
143            Object[] contribs = extension.getContributions();
144            for (Object contrib : contribs) {
145                NotificationDescriptor notifDesc = (NotificationDescriptor) contrib;
146                notificationRegistry.registerNotification(notifDesc, getNames(notifDesc.getEvents()));
147            }
148        } else if (TEMPLATES_EP.equals(xp)) {
149            Object[] contribs = extension.getContributions();
150            for (Object contrib : contribs) {
151                TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib;
152                templateDescriptor.setContext(extension.getContext());
153                registerTemplate(templateDescriptor);
154            }
155        } else if (GENERAL_SETTINGS_EP.equals(xp)) {
156            Object[] contribs = extension.getContributions();
157            for (Object contrib : contribs) {
158                registerGeneralSettings((GeneralSettingsDescriptor) contrib);
159            }
160        } else if (NOTIFICATION_HOOK_EP.equals(xp)) {
161            Object[] contribs = extension.getContributions();
162            for (Object contrib : contribs) {
163                NotificationListenerHookDescriptor desc = (NotificationListenerHookDescriptor) contrib;
164                Class<? extends NotificationListenerHook> clazz = desc.hookListener;
165                try {
166                    NotificationListenerHook hookListener = (NotificationListenerHook) clazz.newInstance();
167                    registerHookListener(desc.name, hookListener);
168                } catch (ReflectiveOperationException e) {
169                    log.error(e);
170                }
171            }
172        } else if (NOTIFICATION_VETO_EP.equals(xp)) {
173            Object[] contribs = extension.getContributions();
174            for (Object contrib : contribs) {
175                NotificationListenerVetoDescriptor desc = (NotificationListenerVetoDescriptor) contrib;
176                notificationVetoRegistry.addContribution(desc);
177            }
178        }
179    }
180
181    private void registerHookListener(String name, NotificationListenerHook hookListener) {
182        hookListeners.put(name, hookListener);
183    }
184
185    protected void registerGeneralSettings(GeneralSettingsDescriptor desc) {
186        generalSettings = desc;
187        String serverPrefix = Framework.expandVars(generalSettings.serverPrefix);
188        if (serverPrefix != null) {
189            generalSettings.serverPrefix = serverPrefix.endsWith("//") ? serverPrefix.substring(0,
190                    serverPrefix.length() - 1) : serverPrefix;
191        }
192        generalSettings.eMailSubjectPrefix = Framework.expandVars(generalSettings.eMailSubjectPrefix);
193        generalSettings.mailSessionJndiName = Framework.expandVars(generalSettings.mailSessionJndiName);
194    }
195
196    private static List<String> getNames(List<NotificationEventDescriptor> events) {
197        List<String> eventNames = new ArrayList<String>();
198        for (NotificationEventDescriptor descriptor : events) {
199            eventNames.add(descriptor.name);
200        }
201        return eventNames;
202    }
203
204    @Override
205    public void unregisterExtension(Extension extension) {
206        String xp = extension.getExtensionPoint();
207        if (NOTIFICATIONS_EP.equals(xp)) {
208            Object[] contribs = extension.getContributions();
209            for (Object contrib : contribs) {
210                NotificationDescriptor notifDesc = (NotificationDescriptor) contrib;
211                notificationRegistry.unregisterNotification(notifDesc, getNames(notifDesc.getEvents()));
212            }
213        } else if (TEMPLATES_EP.equals(xp)) {
214            Object[] contribs = extension.getContributions();
215            for (Object contrib : contribs) {
216                TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib;
217                templateDescriptor.setContext(extension.getContext());
218                unregisterTemplate(templateDescriptor);
219            }
220        } else if (NOTIFICATION_VETO_EP.equals(xp)) {
221            Object[] contribs = extension.getContributions();
222            for (Object contrib : contribs) {
223                NotificationListenerVetoDescriptor vetoDescriptor = (NotificationListenerVetoDescriptor) contrib;
224                notificationVetoRegistry.removeContribution(vetoDescriptor);
225            }
226        }
227    }
228
229    public NotificationRegistry getNotificationRegistry() {
230        return notificationRegistry;
231    }
232
233    public NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() {
234        return notificationVetoRegistry;
235    }
236
237    /**
238     * @deprecated since 7.3
239     * @see NotificationService#getSubscribers(String, DocumentModel)
240     */
241    public List<String> getSubscribers(String notification, String docId) {
242        return getSubscribers(notification, UnrestrictedDocFetcher.fetch(docId));
243    }
244
245    @Override
246    public List<String> getSubscribers(String notification, DocumentModel doc) {
247        return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification);
248    }
249
250    /**
251     * @deprecated since 7.3
252     * @see NotificationService#getSubscriptionsForUserOnDocument(String, DocumentModel)
253     */
254
255    public List<String> getSubscriptionsForUserOnDocument(String username, String docId) {
256        return getSubscriptionsForUserOnDocument(username, UnrestrictedDocFetcher.fetch(docId));
257    }
258
259    @Override
260    public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) {
261        return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username);
262    }
263
264    private void disableEvents(DocumentModel doc) {
265        doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true);
266        doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true);
267        doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true);
268        doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true);
269
270    }
271
272    public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail,
273            NuxeoPrincipal principal, String notificationName) {
274
275        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
276
277            @Override
278            public void run() {
279                doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification);
280                disableEvents(doc);
281                session.saveDocument(doc);
282            }
283
284        };
285
286        runner.runUnrestricted();
287
288        // send event for email if necessary
289        if (sendConfirmationEmail) {
290            raiseConfirmationEvent(principal, doc, username, notificationName);
291        }
292    }
293
294    public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail,
295            NuxeoPrincipal principal) {
296        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
297
298            @Override
299            public void run() {
300                doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username);
301                disableEvents(doc);
302                session.saveDocument(doc);
303            }
304        };
305        runner.runUnrestricted();
306
307        // send event for email if necessary
308        if (sendConfirmationEmail) {
309            raiseConfirmationEvent(principal, doc, username, "All Notifications");
310        }
311    }
312
313    public void removeSubscriptions(String username, List<String> notifications, String docId) {
314        removeSubscriptions(username, notifications, UnrestrictedDocFetcher.fetch(docId));
315    }
316
317    public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc)
318            {
319        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
320
321            @Override
322            public void run() {
323                SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class);
324                for (String notification : notifications) {
325                    sa.removeUserNotificationSubscription(username, notification);
326                }
327                disableEvents(doc);
328                session.saveDocument(doc);
329            }
330        };
331        runner.runUnrestricted();
332
333    }
334
335    protected EventProducer producer;
336
337    protected void doFireEvent(Event event) {
338        if (producer == null) {
339            producer = Framework.getService(EventProducer.class);
340        }
341        producer.fireEvent(event);
342    }
343
344    private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username,
345            String notification) {
346
347        Map<String, Serializable> options = new HashMap<String, Serializable>();
348
349        // Name of the current repository
350        options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName());
351
352        // Add the session ID
353        options.put(CoreEventConstants.SESSION_ID, doc.getSessionId());
354
355        // options for confirmation email
356        options.put("recipients", username);
357        options.put("notifName", notification);
358
359        CoreSession session = doc.getCoreSession();
360        DocumentEventContext ctx = new DocumentEventContext(session, principal, doc);
361        ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY);
362        ctx.setProperties(options);
363        Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED);
364        doFireEvent(event);
365    }
366
367    @Override
368    public void removeSubscription(String username, String notification, String docId) {
369        removeSubscription(username, notification, UnrestrictedDocFetcher.fetch(docId));
370    }
371
372    @Override
373    public void removeSubscription(String username, String notification, DocumentModel doc) {
374        removeSubscriptions(username, Arrays.asList(new String[] { notification }), doc);
375    }
376
377    /**
378     * @param notification
379     * @param docId
380     * @return
381     * @deprecated
382     * @see NotificationService#getSubscribers(String, DocumentModel)
383     */
384    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, String docId)
385            {
386        return getSubscribers(notification, docId);
387    }
388
389    private static void registerTemplate(TemplateDescriptor td) {
390        if (td.src != null && td.src.length() > 0) {
391            URL url = td.getContext().getResource(td.src);
392            TEMPLATES_MAP.put(td.name, url);
393        }
394    }
395
396    private static void unregisterTemplate(TemplateDescriptor td) {
397        if (td.name != null) {
398            TEMPLATES_MAP.remove(td.name);
399        }
400    }
401
402    public static URL getTemplateURL(String name) {
403        return TEMPLATES_MAP.get(name);
404    }
405
406    public String getServerUrlPrefix() {
407        return generalSettings.getServerPrefix();
408    }
409
410    public String getEMailSubjectPrefix() {
411        return generalSettings.getEMailSubjectPrefix();
412    }
413
414    public String getMailSessionJndiName() {
415        return generalSettings.getMailSessionJndiName();
416    }
417
418    public Notification getNotificationByName(String selectedNotification) {
419        List<Notification> listNotif = notificationRegistry.getNotifications();
420        for (Notification notification : listNotif) {
421            if (notification.getName().equals(selectedNotification)) {
422                return notification;
423            }
424        }
425        return null;
426    }
427
428    public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal)
429            {
430
431        Notification notif = getNotificationByName(notificationName);
432
433        NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal);
434        // XXX hack, principals have only one model
435        DataModel model = recipient.getModel().getDataModels().values().iterator().next();
436        String email = (String) model.getData("email");
437        String mailTemplate = notif.getTemplate();
438
439        infoMap.put("mail.to", email);
440
441        String authorUsername = (String) infoMap.get("author");
442
443        if (authorUsername != null) {
444            NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
445            infoMap.put("principalAuthor", author);
446        }
447
448        // mail.put("doc", docMessage); - should be already there
449
450        String subject = notif.getSubject() == null ? "Alert" : notif.getSubject();
451        if (notif.getSubjectTemplate() != null) {
452            subject = notif.getSubjectTemplate();
453        }
454
455        subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject;
456
457        infoMap.put("subject", subject);
458        infoMap.put("template", mailTemplate);
459
460        try {
461            emailHelper.sendmail(infoMap);
462        } catch (MessagingException e) {
463            throw new NuxeoException("Failed to send notification email ", e);
464        }
465    }
466
467    public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment,
468            NuxeoPrincipal sender, List<String> sendTo) {
469        Map<String, Object> infoMap = new HashMap<String, Object>();
470        infoMap.put("document", doc);
471        infoMap.put("subject", subject);
472        infoMap.put("comment", comment);
473        infoMap.put("sender", sender);
474
475        DocumentLocation docLoc = new DocumentLocationImpl(doc);
476        DocumentView docView = new DocumentViewImpl(docLoc);
477        docView.setViewId("view_documents");
478        infoMap.put(
479                "docUrl",
480                getDocLocator().getUrlFromDocumentView(docView, true,
481                        NotificationServiceHelper.getNotificationService().getServerUrlPrefix()));
482
483        if (freemarkerTemplateName == null) {
484            freemarkerTemplateName = "defaultNotifTemplate";
485        }
486        infoMap.put("template", freemarkerTemplateName);
487
488        for (String to : sendTo) {
489            infoMap.put("mail.to", to);
490            try {
491                emailHelper.sendmail(infoMap);
492            } catch (MessagingException e) {
493                log.debug("Failed to send notification email " + e);
494            }
495        }
496    }
497
498    private DocumentViewCodecManager getDocLocator() {
499        if (docLocator == null) {
500            docLocator = Framework.getService(DocumentViewCodecManager.class);
501        }
502        return docLocator;
503    }
504
505    public List<Notification> getNotificationsForSubscriptions(String parentType) {
506        return notificationRegistry.getNotificationsForSubscriptions(parentType);
507    }
508
509    public List<Notification> getNotificationsForEvents(String eventId) {
510        return notificationRegistry.getNotificationsForEvent(eventId);
511    }
512
513    public EmailHelper getEmailHelper() {
514        return emailHelper;
515    }
516
517    public void setEmailHelper(EmailHelper emailHelper) {
518        this.emailHelper = emailHelper;
519    }
520
521    @Override
522    public Set<String> getNotificationEventNames() {
523        return notificationRegistry.getNotificationEventNames();
524    }
525
526    public Collection<NotificationListenerHook> getListenerHooks() {
527        return hookListeners.values();
528    }
529
530    public Collection<NotificationListenerVeto> getNotificationVetos() {
531        return notificationVetoRegistry.getVetos();
532    }
533
534    @Override
535    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc)
536            {
537        return getSubscribers(notification, doc);
538    }
539
540    @Override
541    public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName) {
542        String nxql = String.format("SELECT * FROM Document WHERE ecm:mixinType ='Notifiable' "
543                + "AND notif:notifications/*/subscribers/* = '%s'", prefixedPrincipalName);
544
545        return UnrestrictedDocFetcher.query(nxql);
546    }
547
548}