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 = 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    @Override
230    public NotificationRegistry getNotificationRegistry() {
231        return notificationRegistry;
232    }
233
234    public NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() {
235        return notificationVetoRegistry;
236    }
237
238    /**
239     * @deprecated since 7.3
240     * @see NotificationService#getSubscribers(String, DocumentModel)
241     */
242    @Deprecated
243    @Override
244    public List<String> getSubscribers(String notification, String docId) {
245        return getSubscribers(notification, UnrestrictedDocFetcher.fetch(docId));
246    }
247
248    @Override
249    public List<String> getSubscribers(String notification, DocumentModel doc) {
250        return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification);
251    }
252
253    /**
254     * @deprecated since 7.3
255     * @see NotificationService#getSubscriptionsForUserOnDocument(String, DocumentModel)
256     */
257
258    @Deprecated
259    @Override
260    public List<String> getSubscriptionsForUserOnDocument(String username, String docId) {
261        return getSubscriptionsForUserOnDocument(username, UnrestrictedDocFetcher.fetch(docId));
262    }
263
264    @Override
265    public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) {
266        return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username);
267    }
268
269    private void disableEvents(DocumentModel doc) {
270        doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true);
271        doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true);
272        doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true);
273        doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true);
274
275    }
276
277    @Override
278    public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail,
279            NuxeoPrincipal principal, String notificationName) {
280
281        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
282
283            @Override
284            public void run() {
285                doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification);
286                disableEvents(doc);
287                session.saveDocument(doc);
288            }
289
290        };
291
292        runner.runUnrestricted();
293
294        // send event for email if necessary
295        if (sendConfirmationEmail) {
296            raiseConfirmationEvent(principal, doc, username, notificationName);
297        }
298    }
299
300    @Override
301    public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail,
302            NuxeoPrincipal principal) {
303        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
304
305            @Override
306            public void run() {
307                doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username);
308                disableEvents(doc);
309                session.saveDocument(doc);
310            }
311        };
312        runner.runUnrestricted();
313
314        // send event for email if necessary
315        if (sendConfirmationEmail) {
316            raiseConfirmationEvent(principal, doc, username, "All Notifications");
317        }
318    }
319
320    @Override
321    public void removeSubscriptions(String username, List<String> notifications, String docId) {
322        removeSubscriptions(username, notifications, UnrestrictedDocFetcher.fetch(docId));
323    }
324
325    @Override
326    public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc) {
327        UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(doc.getRepositoryName()) {
328
329            @Override
330            public void run() {
331                SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class);
332                for (String notification : notifications) {
333                    sa.removeUserNotificationSubscription(username, notification);
334                }
335                disableEvents(doc);
336                session.saveDocument(doc);
337            }
338        };
339        runner.runUnrestricted();
340
341    }
342
343    protected EventProducer producer;
344
345    protected void doFireEvent(Event event) {
346        if (producer == null) {
347            producer = Framework.getService(EventProducer.class);
348        }
349        producer.fireEvent(event);
350    }
351
352    private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username,
353            String notification) {
354
355        Map<String, Serializable> options = new HashMap<String, Serializable>();
356
357        // Name of the current repository
358        options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName());
359
360        // Add the session ID
361        options.put(CoreEventConstants.SESSION_ID, doc.getSessionId());
362
363        // options for confirmation email
364        options.put("recipients", username);
365        options.put("notifName", notification);
366
367        CoreSession session = doc.getCoreSession();
368        DocumentEventContext ctx = new DocumentEventContext(session, principal, doc);
369        ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY);
370        ctx.setProperties(options);
371        Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED);
372        doFireEvent(event);
373    }
374
375    @Override
376    public void removeSubscription(String username, String notification, String docId) {
377        removeSubscription(username, notification, UnrestrictedDocFetcher.fetch(docId));
378    }
379
380    @Override
381    public void removeSubscription(String username, String notification, DocumentModel doc) {
382        removeSubscriptions(username, Arrays.asList(new String[] { notification }), doc);
383    }
384
385    /**
386     * @param notification
387     * @param docId
388     * @return
389     * @deprecated
390     * @see NotificationService#getSubscribers(String, DocumentModel)
391     */
392    @Deprecated
393    @Override
394    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, String docId) {
395        return getSubscribers(notification, docId);
396    }
397
398    private static void registerTemplate(TemplateDescriptor td) {
399        if (td.src != null && td.src.length() > 0) {
400            URL url = td.getContext().getResource(td.src);
401            TEMPLATES_MAP.put(td.name, url);
402        }
403    }
404
405    private static void unregisterTemplate(TemplateDescriptor td) {
406        if (td.name != null) {
407            TEMPLATES_MAP.remove(td.name);
408        }
409    }
410
411    public static URL getTemplateURL(String name) {
412        return TEMPLATES_MAP.get(name);
413    }
414
415    public String getServerUrlPrefix() {
416        return generalSettings.getServerPrefix();
417    }
418
419    public String getEMailSubjectPrefix() {
420        return generalSettings.getEMailSubjectPrefix();
421    }
422
423    public String getMailSessionJndiName() {
424        return generalSettings.getMailSessionJndiName();
425    }
426
427    @Override
428    public Notification getNotificationByName(String selectedNotification) {
429        List<Notification> listNotif = notificationRegistry.getNotifications();
430        for (Notification notification : listNotif) {
431            if (notification.getName().equals(selectedNotification)) {
432                return notification;
433            }
434        }
435        return null;
436    }
437
438    @Override
439    public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal) {
440
441        Notification notif = getNotificationByName(notificationName);
442
443        NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal);
444        // XXX hack, principals have only one model
445        DataModel model = recipient.getModel().getDataModels().values().iterator().next();
446        String email = (String) model.getData("email");
447        String mailTemplate = notif.getTemplate();
448
449        infoMap.put("mail.to", email);
450
451        String authorUsername = (String) infoMap.get("author");
452
453        if (authorUsername != null) {
454            NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
455            infoMap.put("principalAuthor", author);
456        }
457
458        // mail.put("doc", docMessage); - should be already there
459
460        String subject = notif.getSubject() == null ? "Alert" : notif.getSubject();
461        if (notif.getSubjectTemplate() != null) {
462            subject = notif.getSubjectTemplate();
463        }
464
465        subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject;
466
467        infoMap.put("subject", subject);
468        infoMap.put("template", mailTemplate);
469
470        try {
471            emailHelper.sendmail(infoMap);
472        } catch (MessagingException e) {
473            throw new NuxeoException("Failed to send notification email ", e);
474        }
475    }
476
477    @Override
478    public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment,
479            NuxeoPrincipal sender, List<String> sendTo) {
480        Map<String, Object> infoMap = new HashMap<String, Object>();
481        infoMap.put("document", doc);
482        infoMap.put("subject", subject);
483        infoMap.put("comment", comment);
484        infoMap.put("sender", sender);
485
486        DocumentLocation docLoc = new DocumentLocationImpl(doc);
487        DocumentView docView = new DocumentViewImpl(docLoc);
488        docView.setViewId("view_documents");
489        infoMap.put(
490                "docUrl",
491                getDocLocator().getUrlFromDocumentView(docView, true,
492                        NotificationServiceHelper.getNotificationService().getServerUrlPrefix()));
493
494        if (freemarkerTemplateName == null) {
495            freemarkerTemplateName = "defaultNotifTemplate";
496        }
497        infoMap.put("template", freemarkerTemplateName);
498
499        for (String to : sendTo) {
500            infoMap.put("mail.to", to);
501            try {
502                emailHelper.sendmail(infoMap);
503            } catch (MessagingException e) {
504                log.debug("Failed to send notification email " + e);
505            }
506        }
507    }
508
509    private DocumentViewCodecManager getDocLocator() {
510        if (docLocator == null) {
511            docLocator = Framework.getService(DocumentViewCodecManager.class);
512        }
513        return docLocator;
514    }
515
516    @Override
517    public List<Notification> getNotificationsForSubscriptions(String parentType) {
518        return notificationRegistry.getNotificationsForSubscriptions(parentType);
519    }
520
521    @Override
522    public List<Notification> getNotificationsForEvents(String eventId) {
523        return notificationRegistry.getNotificationsForEvent(eventId);
524    }
525
526    public EmailHelper getEmailHelper() {
527        return emailHelper;
528    }
529
530    public void setEmailHelper(EmailHelper emailHelper) {
531        this.emailHelper = emailHelper;
532    }
533
534    @Override
535    public Set<String> getNotificationEventNames() {
536        return notificationRegistry.getNotificationEventNames();
537    }
538
539    public Collection<NotificationListenerHook> getListenerHooks() {
540        return hookListeners.values();
541    }
542
543    public Collection<NotificationListenerVeto> getNotificationVetos() {
544        return notificationVetoRegistry.getVetos();
545    }
546
547    @Override
548    public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc) {
549        return getSubscribers(notification, doc);
550    }
551
552    @Override
553    public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName) {
554        String nxql = String.format("SELECT * FROM Document WHERE ecm:mixinType ='Notifiable' "
555                + "AND ecm:isCheckedInVersion = 0 "
556                + "AND notif:notifications/*/subscribers/* = '%s'", prefixedPrincipalName);
557
558        return UnrestrictedDocFetcher.query(nxql);
559    }
560
561}