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