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