001/*
002 * (C) Copyright 2006-2012 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 *     Vladimir Pasquier <vpasquier@nuxeo.com>
017 *
018 */
019
020package org.nuxeo.ecm.platform.ec.notification;
021
022import java.io.Serializable;
023import java.security.Principal;
024import java.util.ArrayList;
025import java.util.Calendar;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033import javax.mail.MessagingException;
034import javax.mail.SendFailedException;
035
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.mvel2.PropertyAccessException;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.DataModel;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.NuxeoGroup;
044import org.nuxeo.ecm.core.api.NuxeoPrincipal;
045import org.nuxeo.ecm.core.api.SystemPrincipal;
046import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
047import org.nuxeo.ecm.core.api.security.SecurityConstants;
048import org.nuxeo.ecm.core.event.Event;
049import org.nuxeo.ecm.core.event.EventBundle;
050import org.nuxeo.ecm.core.event.EventContext;
051import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
052import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
053import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel;
054import org.nuxeo.ecm.core.io.download.DownloadService;
055import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
056import org.nuxeo.ecm.platform.ec.notification.service.NotificationService;
057import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper;
058import org.nuxeo.ecm.platform.notification.api.Notification;
059import org.nuxeo.ecm.platform.url.DocumentViewImpl;
060import org.nuxeo.ecm.platform.url.api.DocumentView;
061import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
062import org.nuxeo.ecm.platform.usermanager.UserManager;
063import org.nuxeo.runtime.api.Framework;
064import org.nuxeo.runtime.api.login.LoginComponent;
065
066public class NotificationEventListener implements PostCommitFilteringEventListener {
067
068    private static final Log log = LogFactory.getLog(NotificationEventListener.class);
069
070    private static final String CHECK_READ_PERMISSION_PROPERTY = "notification.check.read.permission";
071
072    private DocumentViewCodecManager docLocator;
073
074    private UserManager userManager;
075
076    private EmailHelper emailHelper = new EmailHelper();
077
078    private NotificationService notificationService = NotificationServiceHelper.getNotificationService();
079
080    @Override
081    public boolean acceptEvent(Event event) {
082        if (notificationService == null) {
083            return false;
084        }
085        return notificationService.getNotificationEventNames().contains(event.getName());
086    }
087
088    @Override
089    public void handleEvent(EventBundle events) {
090
091        if (notificationService == null) {
092            log.error("Unable to get NotificationService, exiting");
093            return;
094        }
095        boolean processEvents = false;
096        for (String name : notificationService.getNotificationEventNames()) {
097            if (events.containsEventName(name)) {
098                processEvents = true;
099                break;
100            }
101        }
102        if (!processEvents) {
103            return;
104        }
105        for (Event event : events) {
106            Boolean block = (Boolean) event.getContext().getProperty(NotificationConstants.DISABLE_NOTIFICATION_SERVICE);
107            if (block != null && block) {
108                // ignore the event - we are blocked by the caller
109                continue;
110            }
111            List<Notification> notifs = notificationService.getNotificationsForEvents(event.getName());
112            if (notifs != null && !notifs.isEmpty()) {
113                handleNotifications(event, notifs);
114            }
115        }
116
117    }
118
119    protected void handleNotifications(Event event, List<Notification> notifs) {
120
121        EventContext ctx = event.getContext();
122        DocumentEventContext docCtx = null;
123        if (ctx instanceof DocumentEventContext) {
124            docCtx = (DocumentEventContext) ctx;
125        } else {
126            log.warn("Can not handle notification on a event that is not bound to a DocumentEventContext");
127            return;
128        }
129
130        if(docCtx.getSourceDocument() instanceof ShallowDocumentModel) {
131            log.trace("Can not handle notification on a event that is bound to a ShallowDocument");
132            return;
133        }
134
135        CoreSession coreSession = event.getContext().getCoreSession();
136        Map<String, Serializable> properties = event.getContext().getProperties();
137        Map<Notification, List<String>> targetUsers = new HashMap<Notification, List<String>>();
138
139        for (NotificationListenerVeto veto : notificationService.getNotificationVetos()) {
140            if (!veto.accept(event)) {
141                return;
142            }
143        }
144
145        for (NotificationListenerHook hookListener : notificationService.getListenerHooks()) {
146            hookListener.handleNotifications(event);
147        }
148
149        gatherConcernedUsersForDocument(coreSession, docCtx.getSourceDocument(), notifs, targetUsers);
150
151        for (Notification notif : targetUsers.keySet()) {
152            if (!notif.getAutoSubscribed()) {
153                for (String user : targetUsers.get(notif)) {
154                    sendNotificationSignalForUser(notif, user, event, docCtx);
155                }
156            } else {
157                Object recipientProperty = properties.get(NotificationConstants.RECIPIENTS_KEY);
158                String[] recipients = null;
159                if (recipientProperty != null) {
160                    if (recipientProperty instanceof String[]) {
161                        recipients = (String[]) properties.get(NotificationConstants.RECIPIENTS_KEY);
162                    } else if (recipientProperty instanceof String) {
163                        recipients = new String[1];
164                        recipients[0] = (String) recipientProperty;
165                    }
166                }
167                if (recipients == null) {
168                    continue;
169                }
170                Set<String> users = new HashSet<String>();
171                for (String recipient : recipients) {
172                    if (recipient == null) {
173                        continue;
174                    }
175                    if (recipient.contains(NuxeoPrincipal.PREFIX)) {
176                        users.add(recipient.replace(NuxeoPrincipal.PREFIX, ""));
177                    } else if (recipient.contains(NuxeoGroup.PREFIX)) {
178                        List<String> groupMembers = getGroupMembers(recipient.replace(NuxeoGroup.PREFIX, ""));
179                        for (String member : groupMembers) {
180                            users.add(member);
181                        }
182                    } else {
183                        // test if the unprefixed recipient corresponds to a
184                        // group, to fetch its members
185                        if (NotificationServiceHelper.getUsersService().getGroup(recipient) != null) {
186                            users.addAll(getGroupMembers(recipient));
187                        } else {
188                            users.add(recipient);
189                        }
190                    }
191
192                }
193                for (String user : users) {
194                    sendNotificationSignalForUser(notif, user, event, docCtx);
195                }
196
197            }
198        }
199
200    }
201
202    protected UserManager getUserManager() {
203        if (userManager == null) {
204            userManager = Framework.getService(UserManager.class);
205        }
206        return userManager;
207    }
208
209    protected List<String> getGroupMembers(String groupId) {
210        return getUserManager().getUsersInGroupAndSubGroups(groupId);
211    }
212
213    protected void sendNotificationSignalForUser(Notification notification, String subscriptor, Event event,
214            DocumentEventContext ctx) {
215
216        Principal principal;
217        if (LoginComponent.SYSTEM_USERNAME.equals(subscriptor)) {
218            principal = new SystemPrincipal(null);
219        } else {
220            principal = getUserManager().getPrincipal(subscriptor);
221            if (principal == null) {
222                log.error("No Nuxeo principal found for '" + subscriptor
223                        + "'. No notification will be sent to this user");
224                return;
225            }
226        }
227
228        if (Boolean.parseBoolean(Framework.getProperty(CHECK_READ_PERMISSION_PROPERTY))) {
229            if (!ctx.getCoreSession().hasPermission(principal, ctx.getSourceDocument().getRef(), SecurityConstants.READ)) {
230                log.debug("Notification will not be sent: + '" + subscriptor
231                        + "' do not have Read permission on document " + ctx.getSourceDocument().getId());
232                return;
233            }
234        }
235
236        log.debug("Producing notification message.");
237
238        Map<String, Serializable> eventInfo = ctx.getProperties();
239        DocumentModel doc = ctx.getSourceDocument();
240        String author = ctx.getPrincipal().getName();
241        Calendar created = (Calendar) ctx.getSourceDocument().getPropertyValue("dc:created");
242
243        eventInfo.put(NotificationConstants.DESTINATION_KEY, subscriptor);
244        eventInfo.put(NotificationConstants.NOTIFICATION_KEY, notification);
245        eventInfo.put(NotificationConstants.DOCUMENT_ID_KEY, doc.getId());
246        eventInfo.put(NotificationConstants.DATE_TIME_KEY, new Date(event.getTime()));
247        eventInfo.put(NotificationConstants.AUTHOR_KEY, author);
248        eventInfo.put(NotificationConstants.DOCUMENT_VERSION, doc.getVersionLabel());
249        eventInfo.put(NotificationConstants.DOCUMENT_STATE, doc.getCurrentLifeCycleState());
250        eventInfo.put(NotificationConstants.DOCUMENT_CREATED, created.getTime());
251        StringBuilder userUrl = new StringBuilder();
252        userUrl.append(notificationService.getServerUrlPrefix()).append("user/").append(ctx.getPrincipal().getName());
253        eventInfo.put(NotificationConstants.USER_URL_KEY, userUrl.toString());
254        eventInfo.put(NotificationConstants.DOCUMENT_LOCATION, doc.getPathAsString());
255        // Main file link for downloading
256        BlobHolder bh = doc.getAdapter(BlobHolder.class);
257        if (bh != null && bh.getBlob() != null) {
258            DownloadService downloadService = Framework.getService(DownloadService.class);
259            String filename = bh.getBlob().getFilename();
260            String docMainFile = notificationService.getServerUrlPrefix()
261                    + downloadService.getDownloadUrl(doc, DownloadService.BLOBHOLDER_0, filename);
262            eventInfo.put(NotificationConstants.DOCUMENT_MAIN_FILE, docMainFile);
263        }
264
265        if (!isDeleteEvent(event.getName())) {
266            DocumentView docView = new DocumentViewImpl(doc);
267            DocumentViewCodecManager docLocator = getDocLocator();
268            if (docLocator != null) {
269                eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY,
270                        getDocLocator().getUrlFromDocumentView(docView, true, notificationService.getServerUrlPrefix()));
271            } else {
272                eventInfo.put(NotificationConstants.DOCUMENT_URL_KEY, "");
273            }
274            eventInfo.put(NotificationConstants.DOCUMENT_TITLE_KEY, doc.getTitle());
275        }
276
277        if (isInterestedInNotification(notification)) {
278            sendNotification(event, ctx);
279            if (log.isDebugEnabled()) {
280                log.debug("notification " + notification.getName() + " sent to " + notification.getSubject());
281            }
282        }
283    }
284
285    public void sendNotification(Event event, DocumentEventContext ctx) {
286
287        String eventId = event.getName();
288        log.debug("Received a message for notification sender with eventId : " + eventId);
289
290        Map<String, Serializable> eventInfo = ctx.getProperties();
291        String userDest = (String) eventInfo.get(NotificationConstants.DESTINATION_KEY);
292        NotificationImpl notif = (NotificationImpl) eventInfo.get(NotificationConstants.NOTIFICATION_KEY);
293
294        // send email
295        NuxeoPrincipal recepient = NotificationServiceHelper.getUsersService().getPrincipal(userDest);
296        if (recepient == null) {
297            log.error("Couldn't find user: " + userDest + " to send her a mail.");
298            return;
299        }
300        // XXX hack, principals have only one model
301        DataModel model = recepient.getModel().getDataModels().values().iterator().next();
302        String email = (String) model.getData("email");
303        if (email == null || "".equals(email)) {
304            log.error("No email found for user: " + userDest);
305            return;
306        }
307
308        String subjectTemplate = notif.getSubjectTemplate();
309
310        String mailTemplate = null;
311        // mail template can be dynamically computed from a MVEL expression
312        if (notif.getTemplateExpr() != null) {
313            try {
314                mailTemplate = emailHelper.evaluateMvelExpresssion(notif.getTemplateExpr(), eventInfo);
315            } catch (PropertyAccessException pae) {
316                if (log.isDebugEnabled()) {
317                    log.debug("Cannot evaluate mail template expression '" + notif.getTemplateExpr()
318                            + "' in that context " + eventInfo, pae);
319                }
320            }
321        }
322        // if there is no mailTemplate evaluated, use the defined one
323        if (StringUtils.isEmpty(mailTemplate)) {
324            mailTemplate = notif.getTemplate();
325        }
326
327        log.debug("email: " + email);
328        log.debug("mail template: " + mailTemplate);
329        log.debug("subject template: " + subjectTemplate);
330
331        Map<String, Object> mail = new HashMap<String, Object>();
332        mail.put("mail.to", email);
333
334        String authorUsername = (String) eventInfo.get(NotificationConstants.AUTHOR_KEY);
335
336        if (authorUsername != null) {
337            NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
338            mail.put(NotificationConstants.PRINCIPAL_AUTHOR_KEY, author);
339        }
340
341        mail.put(NotificationConstants.DOCUMENT_KEY, ctx.getSourceDocument());
342        String subject = notif.getSubject() == null ? NotificationConstants.NOTIFICATION_KEY : notif.getSubject();
343        subject = notificationService.getEMailSubjectPrefix() + subject;
344        mail.put("subject", subject);
345        mail.put("template", mailTemplate);
346        mail.put("subjectTemplate", subjectTemplate);
347
348        // Transferring all data from event to email
349        for (String key : eventInfo.keySet()) {
350            mail.put(key, eventInfo.get(key) == null ? "" : eventInfo.get(key));
351            log.debug("Mail prop: " + key);
352        }
353
354        mail.put(NotificationConstants.EVENT_ID_KEY, eventId);
355
356        try {
357            emailHelper.sendmail(mail);
358        } catch (MessagingException e) {
359            String cause = "";
360            if ((e instanceof SendFailedException) && (e.getCause() instanceof SendFailedException)) {
361                cause = " - Cause: " + e.getCause().getMessage();
362            }
363            log.warn("Failed to send notification email to '" + email + "': " + e.getClass().getName() + ": "
364                    + e.getMessage() + cause);
365        }
366    }
367
368    /**
369     * Adds the concerned users to the list of targeted users for these notifications.
370     */
371    private void gatherConcernedUsersForDocument(CoreSession coreSession, DocumentModel doc, List<Notification> notifs,
372            Map<Notification, List<String>> targetUsers) {
373        if (doc.getPath().segmentCount() > 1) {
374            log.debug("Searching document: " + doc.getName());
375            getInterstedUsers(doc, notifs, targetUsers);
376            if (doc.getParentRef() != null && coreSession.exists(doc.getParentRef())) {
377                DocumentModel parent = getDocumentParent(coreSession, doc);
378                gatherConcernedUsersForDocument(coreSession, parent, notifs, targetUsers);
379            }
380        }
381    }
382
383    private DocumentModel getDocumentParent(CoreSession coreSession, DocumentModel doc) {
384        if (doc == null) {
385            return null;
386        }
387        return coreSession.getDocument(doc.getParentRef());
388    }
389
390    private void getInterstedUsers(DocumentModel doc, List<Notification> notifs,
391            Map<Notification, List<String>> targetUsers) {
392        for (Notification notification : notifs) {
393            if (!notification.getAutoSubscribed()) {
394                List<String> userGroup = notificationService.getSubscribers(notification.getName(), doc);
395                for (String subscriptor : userGroup) {
396                    if (subscriptor != null) {
397                        if (isUser(subscriptor)) {
398                            storeUserForNotification(notification, subscriptor.substring(5), targetUsers);
399                        } else {
400                            // it is a group - get all users and send
401                            // notifications to them
402                            List<String> usersOfGroup = getGroupMembers(subscriptor.substring(6));
403                            if (usersOfGroup != null && !usersOfGroup.isEmpty()) {
404                                for (String usr : usersOfGroup) {
405                                    storeUserForNotification(notification, usr, targetUsers);
406                                }
407                            }
408                        }
409                    }
410                }
411            } else {
412                // An automatic notification happens
413                // should be sent to interested users
414                targetUsers.put(notification, new ArrayList<String>());
415            }
416        }
417    }
418
419    private static void storeUserForNotification(Notification notification, String user,
420            Map<Notification, List<String>> targetUsers) {
421        List<String> subscribedUsers = targetUsers.get(notification);
422        if (subscribedUsers == null) {
423            targetUsers.put(notification, new ArrayList<String>());
424        }
425        if (!targetUsers.get(notification).contains(user)) {
426            targetUsers.get(notification).add(user);
427        }
428    }
429
430    private boolean isDeleteEvent(String eventId) {
431        List<String> deletionEvents = new ArrayList<String>();
432        deletionEvents.add("aboutToRemove");
433        deletionEvents.add("documentRemoved");
434        return deletionEvents.contains(eventId);
435    }
436
437    private boolean isUser(String subscriptor) {
438        return subscriptor != null && subscriptor.startsWith("user:");
439    }
440
441    public boolean isInterestedInNotification(Notification notif) {
442        return notif != null && "email".equals(notif.getChannel());
443    }
444
445    public DocumentViewCodecManager getDocLocator() {
446        if (docLocator == null) {
447            docLocator = Framework.getService(DocumentViewCodecManager.class);
448        }
449        return docLocator;
450    }
451
452    public EmailHelper getEmailHelper() {
453        return emailHelper;
454    }
455
456    public void setEmailHelper(EmailHelper emailHelper) {
457        this.emailHelper = emailHelper;
458    }
459
460}