/*
* (C) Copyright 2007-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*/
package org.nuxeo.ecm.platform.ec.notification.service;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.mail.MessagingException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentLocation;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.event.CoreEventConstants;
import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventProducer;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.versioning.VersioningService;
import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService;
import org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener;
import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
import org.nuxeo.ecm.platform.ec.notification.NotificationListenerHook;
import org.nuxeo.ecm.platform.ec.notification.NotificationListenerVeto;
import org.nuxeo.ecm.platform.ec.notification.SubscriptionAdapter;
import org.nuxeo.ecm.platform.ec.notification.email.EmailHelper;
import org.nuxeo.ecm.platform.notification.api.Notification;
import org.nuxeo.ecm.platform.notification.api.NotificationManager;
import org.nuxeo.ecm.platform.notification.api.NotificationRegistry;
import org.nuxeo.ecm.platform.url.DocumentViewImpl;
import org.nuxeo.ecm.platform.url.api.DocumentView;
import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.ComponentName;
import org.nuxeo.runtime.model.DefaultComponent;
import org.nuxeo.runtime.model.Extension;
/**
* @author <a href="mailto:npaslaru@nuxeo.com">Narcis Paslaru</a>
*/
public class NotificationService extends DefaultComponent implements NotificationManager {
public static final ComponentName NAME = new ComponentName(
"org.nuxeo.ecm.platform.ec.notification.service.NotificationService");
private static final Log log = LogFactory.getLog(NotificationService.class);
public static final String SUBSCRIPTION_NAME = "UserSubscription";
protected static final String NOTIFICATIONS_EP = "notifications";
protected static final String TEMPLATES_EP = "templates";
protected static final String GENERAL_SETTINGS_EP = "generalSettings";
protected static final String NOTIFICATION_HOOK_EP = "notificationListenerHook";
protected static final String NOTIFICATION_VETO_EP = "notificationListenerVeto";
// FIXME: performance issue when putting URLs in a Map.
protected static final Map<String, URL> TEMPLATES_MAP = new HashMap<>();
protected EmailHelper emailHelper = new EmailHelper();
protected GeneralSettingsDescriptor generalSettings;
protected NotificationRegistry notificationRegistry;
protected DocumentViewCodecManager docLocator;
protected final Map<String, NotificationListenerHook> hookListeners = new HashMap<>();
protected NotificationListenerVetoRegistry notificationVetoRegistry;
@Override
@SuppressWarnings("unchecked")
public <T> T getAdapter(Class<T> adapter) {
if (adapter.isAssignableFrom(NotificationManager.class)) {
return (T) this;
}
return null;
}
@Override
public void activate(ComponentContext context) {
notificationRegistry = new NotificationRegistryImpl();
notificationVetoRegistry = new NotificationListenerVetoRegistry();
// init default settings
generalSettings = new GeneralSettingsDescriptor();
generalSettings.serverPrefix = "http://localhost:8080/nuxeo/";
generalSettings.eMailSubjectPrefix = "[Nuxeo]";
generalSettings.mailSessionJndiName = "java:/Mail";
}
@Override
public void deactivate(ComponentContext context) {
notificationRegistry.clear();
notificationVetoRegistry.clear();
notificationRegistry = null;
notificationVetoRegistry = null;
}
@Override
public void registerExtension(Extension extension) {
log.info("Registering notification extension");
String xp = extension.getExtensionPoint();
if (NOTIFICATIONS_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
NotificationDescriptor notifDesc = (NotificationDescriptor) contrib;
notificationRegistry.registerNotification(notifDesc, getNames(notifDesc.getEvents()));
}
} else if (TEMPLATES_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib;
templateDescriptor.setContext(extension.getContext());
registerTemplate(templateDescriptor);
}
} else if (GENERAL_SETTINGS_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
registerGeneralSettings((GeneralSettingsDescriptor) contrib);
}
} else if (NOTIFICATION_HOOK_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
NotificationListenerHookDescriptor desc = (NotificationListenerHookDescriptor) contrib;
Class<? extends NotificationListenerHook> clazz = desc.hookListener;
try {
NotificationListenerHook hookListener = clazz.newInstance();
registerHookListener(desc.name, hookListener);
} catch (ReflectiveOperationException e) {
log.error(e);
}
}
} else if (NOTIFICATION_VETO_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
NotificationListenerVetoDescriptor desc = (NotificationListenerVetoDescriptor) contrib;
notificationVetoRegistry.addContribution(desc);
}
}
}
private void registerHookListener(String name, NotificationListenerHook hookListener) {
hookListeners.put(name, hookListener);
}
protected void registerGeneralSettings(GeneralSettingsDescriptor desc) {
generalSettings = desc;
String serverPrefix = Framework.expandVars(generalSettings.serverPrefix);
if (serverPrefix != null) {
generalSettings.serverPrefix = serverPrefix.endsWith("//")
? serverPrefix.substring(0, serverPrefix.length() - 1) : serverPrefix;
}
generalSettings.eMailSubjectPrefix = Framework.expandVars(generalSettings.eMailSubjectPrefix);
generalSettings.mailSessionJndiName = Framework.expandVars(generalSettings.mailSessionJndiName);
}
private static List<String> getNames(List<NotificationEventDescriptor> events) {
List<String> eventNames = new ArrayList<>();
for (NotificationEventDescriptor descriptor : events) {
eventNames.add(descriptor.name);
}
return eventNames;
}
@Override
public void unregisterExtension(Extension extension) {
String xp = extension.getExtensionPoint();
if (NOTIFICATIONS_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
NotificationDescriptor notifDesc = (NotificationDescriptor) contrib;
notificationRegistry.unregisterNotification(notifDesc, getNames(notifDesc.getEvents()));
}
} else if (TEMPLATES_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
TemplateDescriptor templateDescriptor = (TemplateDescriptor) contrib;
templateDescriptor.setContext(extension.getContext());
unregisterTemplate(templateDescriptor);
}
} else if (NOTIFICATION_VETO_EP.equals(xp)) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
NotificationListenerVetoDescriptor vetoDescriptor = (NotificationListenerVetoDescriptor) contrib;
notificationVetoRegistry.removeContribution(vetoDescriptor);
}
}
}
public NotificationListenerVetoRegistry getNotificationListenerVetoRegistry() {
return notificationVetoRegistry;
}
@Override
public List<String> getSubscribers(String notification, DocumentModel doc) {
return doc.getAdapter(SubscriptionAdapter.class).getNotificationSubscribers(notification);
}
@Override
public List<String> getSubscriptionsForUserOnDocument(String username, DocumentModel doc) {
return doc.getAdapter(SubscriptionAdapter.class).getUserSubscriptions(username);
}
private void disableEvents(DocumentModel doc) {
doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true);
doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true);
doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true);
doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true);
}
@Override
public void addSubscription(String username, String notification, DocumentModel doc, Boolean sendConfirmationEmail,
NuxeoPrincipal principal, String notificationName) {
CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> {
doc.getAdapter(SubscriptionAdapter.class).addSubscription(username, notification);
disableEvents(doc);
session.saveDocument(doc);
});
// send event for email if necessary
if (sendConfirmationEmail) {
raiseConfirmationEvent(principal, doc, username, notificationName);
}
}
@Override
public void addSubscriptions(String username, DocumentModel doc, Boolean sendConfirmationEmail,
NuxeoPrincipal principal) {
CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> {
doc.getAdapter(SubscriptionAdapter.class).addSubscriptionsToAll(username);
disableEvents(doc);
session.saveDocument(doc);
});
// send event for email if necessary
if (sendConfirmationEmail) {
raiseConfirmationEvent(principal, doc, username, "All Notifications");
}
}
@Override
public void removeSubscriptions(String username, List<String> notifications, DocumentModel doc) {
CoreInstance.doPrivileged(doc.getRepositoryName(), (CoreSession session) -> {
SubscriptionAdapter sa = doc.getAdapter(SubscriptionAdapter.class);
for (String notification : notifications) {
sa.removeUserNotificationSubscription(username, notification);
}
disableEvents(doc);
session.saveDocument(doc);
});
}
protected EventProducer producer;
protected void doFireEvent(Event event) {
if (producer == null) {
producer = Framework.getService(EventProducer.class);
}
producer.fireEvent(event);
}
private void raiseConfirmationEvent(NuxeoPrincipal principal, DocumentModel doc, String username,
String notification) {
Map<String, Serializable> options = new HashMap<>();
// Name of the current repository
options.put(CoreEventConstants.REPOSITORY_NAME, doc.getRepositoryName());
// Add the session ID
options.put(CoreEventConstants.SESSION_ID, doc.getSessionId());
// options for confirmation email
options.put("recipients", username);
options.put("notifName", notification);
CoreSession session = doc.getCoreSession();
DocumentEventContext ctx = new DocumentEventContext(session, principal, doc);
ctx.setCategory(DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY);
ctx.setProperties(options);
Event event = ctx.newEvent(DocumentEventTypes.SUBSCRIPTION_ASSIGNED);
doFireEvent(event);
}
@Override
public void removeSubscription(String username, String notification, DocumentModel doc) {
removeSubscriptions(username, Arrays.asList(notification), doc);
}
private static void registerTemplate(TemplateDescriptor td) {
if (td.src != null && td.src.length() > 0) {
URL url = td.getContext().getResource(td.src);
TEMPLATES_MAP.put(td.name, url);
}
}
private static void unregisterTemplate(TemplateDescriptor td) {
if (td.name != null) {
TEMPLATES_MAP.remove(td.name);
}
}
public static URL getTemplateURL(String name) {
return TEMPLATES_MAP.get(name);
}
public String getServerUrlPrefix() {
return generalSettings.getServerPrefix();
}
public String getEMailSubjectPrefix() {
return generalSettings.getEMailSubjectPrefix();
}
public String getMailSessionJndiName() {
return generalSettings.getMailSessionJndiName();
}
@Override
public Notification getNotificationByName(String selectedNotification) {
List<Notification> listNotif = notificationRegistry.getNotifications();
for (Notification notification : listNotif) {
if (notification.getName().equals(selectedNotification)) {
return notification;
}
}
return null;
}
@Override
public void sendNotification(String notificationName, Map<String, Object> infoMap, String userPrincipal) {
Notification notif = getNotificationByName(notificationName);
NuxeoPrincipal recipient = NotificationServiceHelper.getUsersService().getPrincipal(userPrincipal);
String email = recipient.getEmail();
String mailTemplate = notif.getTemplate();
infoMap.put("mail.to", email);
String authorUsername = (String) infoMap.get("author");
if (authorUsername != null) {
NuxeoPrincipal author = NotificationServiceHelper.getUsersService().getPrincipal(authorUsername);
infoMap.put("principalAuthor", author);
}
// mail.put("doc", docMessage); - should be already there
String subject = notif.getSubject() == null ? "Alert" : notif.getSubject();
if (notif.getSubjectTemplate() != null) {
subject = notif.getSubjectTemplate();
}
subject = NotificationServiceHelper.getNotificationService().getEMailSubjectPrefix() + " " + subject;
infoMap.put("subject", subject);
infoMap.put("template", mailTemplate);
try {
emailHelper.sendmail(infoMap);
} catch (MessagingException e) {
throw new NuxeoException("Failed to send notification email ", e);
}
}
@Override
public void sendDocumentByMail(DocumentModel doc, String freemarkerTemplateName, String subject, String comment,
NuxeoPrincipal sender, List<String> sendTo) {
Map<String, Object> infoMap = new HashMap<>();
infoMap.put("document", doc);
infoMap.put("subject", subject);
infoMap.put("comment", comment);
infoMap.put("sender", sender);
DocumentLocation docLoc = new DocumentLocationImpl(doc);
DocumentView docView = new DocumentViewImpl(docLoc);
docView.setViewId("view_documents");
infoMap.put("docUrl", getDocLocator().getUrlFromDocumentView(docView, true,
NotificationServiceHelper.getNotificationService().getServerUrlPrefix()));
if (freemarkerTemplateName == null) {
freemarkerTemplateName = "defaultNotifTemplate";
}
infoMap.put("template", freemarkerTemplateName);
for (String to : sendTo) {
infoMap.put("mail.to", to);
try {
emailHelper.sendmail(infoMap);
} catch (MessagingException e) {
log.debug("Failed to send notification email " + e);
}
}
}
private DocumentViewCodecManager getDocLocator() {
if (docLocator == null) {
docLocator = Framework.getService(DocumentViewCodecManager.class);
}
return docLocator;
}
@Override
public List<Notification> getNotificationsForSubscriptions(String parentType) {
return notificationRegistry.getNotificationsForSubscriptions(parentType);
}
@Override
public List<Notification> getNotificationsForEvents(String eventId) {
return notificationRegistry.getNotificationsForEvent(eventId);
}
public EmailHelper getEmailHelper() {
return emailHelper;
}
public void setEmailHelper(EmailHelper emailHelper) {
this.emailHelper = emailHelper;
}
@Override
public Set<String> getNotificationEventNames() {
return notificationRegistry.getNotificationEventNames();
}
public Collection<NotificationListenerHook> getListenerHooks() {
return hookListeners.values();
}
public Collection<NotificationListenerVeto> getNotificationVetos() {
return notificationVetoRegistry.getVetos();
}
@Override
public List<String> getUsersSubscribedToNotificationOnDocument(String notification, DocumentModel doc) {
return getSubscribers(notification, doc);
}
@Override
public List<DocumentModel> getSubscribedDocuments(String prefixedPrincipalName, String repositoryName) {
String nxql = "SELECT * FROM Document WHERE ecm:mixinType = '" + SubscriptionAdapter.NOTIFIABLE_FACET + "' "
+ "AND ecm:isCheckedInVersion = 0 " + "AND notif:notifications/*/subscribers/* = "
+ NXQL.escapeString(prefixedPrincipalName);
return CoreInstance.doPrivileged(repositoryName,
(CoreSession s) -> s.query(nxql).stream().map(NotificationService::detachDocumentModel).collect(
Collectors.toList()));
}
protected static DocumentModel detachDocumentModel(DocumentModel doc) {
doc.detach(true);
return doc;
}
}