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